diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..763218c12dfd87a6800cd2f8388f71310d58f34b
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,7 @@
+Copyright 2022-2023, MIT Center for Bits and Atoms
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software, hardware and associated documentation files (the "Contribution"), to deal in the Contribution without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Contribution, and to permit persons to whom the Contribution is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Contribution.
+
+THE CONTRIBUTION IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE CONTRIBUTION OR THE USE OR OTHER DEALINGS IN THE CONTRIBUTION.
\ No newline at end of file
diff --git a/README.md b/README.md
index b4daa037a0b4ffe76235ef68b6ec0ea92d41377a..b843bbe73a2c87adf8675434b31440dc7004b561 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,33 @@
-## Learning Polymer Flows 
+## Online Measurement for Parameter Discovery in FFF 
 
-In order to automatically guess at print parameters (flowrate and nozzle temperatures), we developed an extruder that features a filament sensor (to detect real flowrates) and a pressure sensor, and used it to measure a swath of that parameter space using a simple routine, fit that data to a predictive function, and used a meta-heuristic to extract real parameters from those functions. We were then able to automatically determine print parameters for a number of new filaments that we had not previously tested. 
+In order to automatically guess at print parameters (flowrate and nozzle temperatures), we developed an extruder that features a filament sensor (to detect real flowrates) and a pressure sensor. We used it to measure a swath of that parameter space using a simple routine, fit that data to a predictive function, and use a meta-heuristic to extract real parameters from those functions. We were then able to automatically determine print parameters for a number of new filaments that we had not previously tested. 
 
-![extruder](writing/figures/exp/extruder-labelled.png)
+![extruder](images/extruder-labelled.png)
 > Above is our extruder, featuring a loadcell to measure pressure (C), a filament sensor (A) that measures filament width and linear feedrate, and a COTS hotend (D) and drive gears (B). 
 
-![individuals](writing/figures/individual-fits.png)
+![individuals](images/individual-fits-colorblind.png)
 > We collect data with a simple routine that sets the nozzle at its maximum temperature, then begins flowing plastic at a set rate, and turns off the heater. Pressure and temperature data are simultaneously collected as the nozzle cools (and as pressure increases). 
 
-![contour](writing/figures/contour-fit-abs-08.png)
+![contour](images/contour-fit-abs-08-colorblind.png)
 > We can expand this fit into a full contour of the parameter space, and then select operating points using a meta heuristic. 
 
-![method](writing/figures/methods-summary.png)
+![method](images/methods-summary.png)
 
-![outputs](writing/figures/outputs.png)
+![outputs](images/outputs.png)
 > Using this method, we were able to produce a series of test articles using filaments that we had not previously tested. 
 
 ### In this Repo
 
 **Analysis and Data**
 
-We have data an analysis in the [analysis/](analysis) folder, with the most relevant data (from our paper) in [analysis/flows/spindown/](analysis/flows/spindown). 
+We have data an analysis in the [analysis/](analysis) folder, and data sets stored in [data/](analysis/flows/spindown). Analysis codes (Jupyter Notebooks) run with these data. 
 
-Data there is organized into `material-nozzleSize` folders, with raw data in `.json` format and cleaned data sets available as python pickles, which import as pandas dataframes (IIRC). Data was cleaned using [this script](analysis/flows/2023-06-01_spindown-data-cleanup.ipynb) and analyzed using [this one](analysis/flows/2023-06-01_spindown-final-fits.ipynb). 
+Data there is organized into `material-nozzleSize` folders, with raw data in `.json` format and cleaned data sets available as python pickles, which import as pandas dataframes. Data was cleaned using [this script](analysis/data-cleanup.ipynb) and analyzed using [this one](analysis/data-fitting.ipynb). 
 
 **Experimental System**
 
-The [system folder](system) contains the javascript controller as well as firmwares for the relevant hardware. Hardware from these experiments is now out of date; I can produce CAD and circuit design files if pressed, but sorting and documenting these things is more of a challenge. 
+The [system folder](system) contains the javascript controller as well as firmwares for the relevant hardware. 
 
 **The Paper**
 
-Writing is in... [writing/](writing), though it happens in overleaf - I will keep drafts and source files backed up here. The [most recent draft is here](writing/drafts/2023-08-21_Online_Measurement_for_Parameter_Discovery_in_FFF.pdf)
-
-### Future Work
-
-~ we want to optimize printer parameters, but this inevitably leads to the whole printing process... i.e. given what we know about slumping and layer adhesion, we actually have as a kind of ~ primary input (un-surprisingly) the geometry itself: a critical factor is the inter-layer time, and that varies greatly with geometry... 
-
-So we end up with this loop that deconstructs a print layer-by-layer, builds (for each) a model of the layer's print time, given requested rates (which is calculated using the machine's own lookahead controller, basically a full sim of the machine's dynamics), then for each layer we find an optimal temperature-and-rate pair, given machine/material models we learn about the nozzle / extruder (how fast can we push, at what temperatuers?) and about the cooling properties (for slumping, to know how much time-to-cool each layer will need, given the temperature it's printed at). 
-
-![ext-loops](log/images/2022-09-30_the-big-outer-loop-draft.png)
-
-The final challenge is that each layer has a different optimal temperature... Rates are easy enough to change layer-by-layer, but the machine's hotend can't slew as quickly; so we want to basically run a lookahead through layers to analyze these and find new targets... simpler option is to pick whichever min/max and run the print there. 
-
-## Stubs 
-
-### The Slumping Model (Slump-Sweeping)
-
-![drop-opt](drawings/lpf-layer-pressure.png)
-![drop-opt](drawings/lpf-pressure-drop-proto.png)
-
-![slumping-pressure-drop](log/videos/2022-08_pressure-drop-printing-short-enc.mp4)
-
-![plot](drawings/2022-10-27_prelim-slumping-plot-labels.png)
-![threeup](drawings/2022-10-27_prelim-slumping-three-plots.png)
-
-### Surface Finish and Pressure Consistency ?
-
-![pres](drawings/2022-10-27_hair-plots.png)
-
-### The Thermal Model and Lookahead
-
-We also need to figure, roughly, how fast we can slew nozzle temps up & down; this lets us do a fit of thermal-profiles to layer-rates, which is some ~ tricky dynamic programming I think... and will probably be real fun. 
-
-Also related is a new motion controller... that does network-model estimation inline with dynamics; 
-
-![lookahead](log/images/2022-09-16_axl-queue-structures.png)
-![nets](log/images/2022-09-19_axl-net-structures.png)
-
-### Dependencies 
-
-- [clank fxy](https://gitlab.cba.mit.edu/jakeread/clank-fxy)
-- [glue gun hotend](https://gitlab.cba.mit.edu/jakeread/glue-gun) 
-- [filament sensor](https://gitlab.cba.mit.edu/jakeread/filament-sensor)
-- [heat bed](https://gitlab.cba.mit.edu/jakeread/print-bed)
-- [loadcell amps](https://gitlab.cba.mit.edu/jakeread/loadcell-amp)
-- [heater module](https://gitlab.cba.mit.edu/jakeread/heater-module/)
+A draft of the paper is located [here](paper/Online_Measurement_for_Parameter_Discovery_in_FFF_2023-11-01.pdf).
diff --git a/images/contour-fit-abs-08-colorblind.png b/images/contour-fit-abs-08-colorblind.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dc9008497c5d44ab525050abf5cddcbd4ea6850
Binary files /dev/null and b/images/contour-fit-abs-08-colorblind.png differ
diff --git a/images/ex-extrusion-diagram.png b/images/ex-extrusion-diagram.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f9f8f1180da6ff3c7109d5a137f94efe9b33d97
Binary files /dev/null and b/images/ex-extrusion-diagram.png differ
diff --git a/images/extruder-labelled.png b/images/extruder-labelled.png
new file mode 100644
index 0000000000000000000000000000000000000000..a248e6a85b0182e406dba84e4fc3bef90df13f42
Binary files /dev/null and b/images/extruder-labelled.png differ
diff --git a/images/individual-fits-colorblind.png b/images/individual-fits-colorblind.png
new file mode 100644
index 0000000000000000000000000000000000000000..91ff50028ba19aab028be916ccd6fa659dc208c4
Binary files /dev/null and b/images/individual-fits-colorblind.png differ
diff --git a/images/methods-summary.png b/images/methods-summary.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6ea33cc36c05210d3f1aa4d96465974a64fa1cc
Binary files /dev/null and b/images/methods-summary.png differ
diff --git a/images/outputs.png b/images/outputs.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a3bebe43dddcff5adf092d8fdfd1707967b9ea8
Binary files /dev/null and b/images/outputs.png differ
diff --git a/images/temp-dropoffs.png b/images/temp-dropoffs.png
new file mode 100644
index 0000000000000000000000000000000000000000..76918946e6dbe669f38880028f665e9d094fdaef
Binary files /dev/null and b/images/temp-dropoffs.png differ
diff --git a/paper/Online_Measurement_for_Parameter_Discovery_in_FFF_2023-11-01.pdf b/paper/Online_Measurement_for_Parameter_Discovery_in_FFF_2023-11-01.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..7bcea2c04c75acf910ac69262cbab721c01622c7
Binary files /dev/null and b/paper/Online_Measurement_for_Parameter_Discovery_in_FFF_2023-11-01.pdf differ
diff --git a/system/cad/AS_Glue-Gun v18.f3z b/system/cad/AS_Glue-Gun v18.f3z
new file mode 100644
index 0000000000000000000000000000000000000000..c14fc09e7c1aa6b411415a1bfa15360b26437313
Binary files /dev/null and b/system/cad/AS_Glue-Gun v18.f3z differ
diff --git a/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1.brd b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1.brd
new file mode 100644
index 0000000000000000000000000000000000000000..3588146b70644006f5c64530c75a7e88dbd77d59
--- /dev/null
+++ b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1.brd	
@@ -0,0 +1,1426 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE eagle SYSTEM "eagle.dtd">
+<eagle version="9.7.0">
+<drawing>
+<settings>
+<setting alwaysvectorfont="no"/>
+<setting verticaltext="up"/>
+</settings>
+<grid distance="0.5" unitdist="mm" unit="mm" style="lines" multiple="1" display="yes" altdistance="5" altunitdist="mil" altunit="mil"/>
+<layers>
+<layer number="1" name="Top" color="4" fill="1" visible="yes" active="yes"/>
+<layer number="2" name="Route2" color="16" fill="1" visible="no" active="no"/>
+<layer number="3" name="Route3" color="17" fill="1" visible="no" active="no"/>
+<layer number="4" name="Route4" color="18" fill="1" visible="no" active="no"/>
+<layer number="5" name="Route5" color="19" fill="1" visible="no" active="no"/>
+<layer number="6" name="Route6" color="25" fill="1" visible="no" active="no"/>
+<layer number="7" name="Route7" color="26" fill="1" visible="no" active="no"/>
+<layer number="8" name="Route8" color="27" fill="1" visible="no" active="no"/>
+<layer number="9" name="Route9" color="28" fill="1" visible="no" active="no"/>
+<layer number="10" name="Route10" color="29" fill="1" visible="no" active="no"/>
+<layer number="11" name="Route11" color="30" fill="1" visible="no" active="no"/>
+<layer number="12" name="Route12" color="20" fill="1" visible="no" active="no"/>
+<layer number="13" name="Route13" color="21" fill="1" visible="no" active="no"/>
+<layer number="14" name="Route14" color="22" fill="1" visible="no" active="no"/>
+<layer number="15" name="Route15" color="23" fill="1" visible="no" active="no"/>
+<layer number="16" name="Bottom" color="1" fill="1" visible="yes" active="yes"/>
+<layer number="17" name="Pads" color="2" fill="1" visible="yes" active="yes"/>
+<layer number="18" name="Vias" color="2" fill="1" visible="yes" active="yes"/>
+<layer number="19" name="Unrouted" color="6" fill="1" visible="yes" active="yes"/>
+<layer number="20" name="Dimension" color="24" fill="1" visible="yes" active="yes"/>
+<layer number="21" name="tPlace" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="22" name="bPlace" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="23" name="tOrigins" color="15" fill="1" visible="yes" active="yes"/>
+<layer number="24" name="bOrigins" color="15" fill="1" visible="yes" active="yes"/>
+<layer number="25" name="tNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="26" name="bNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="27" name="tValues" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="28" name="bValues" color="7" fill="1" visible="no" active="yes"/>
+<layer number="29" name="tStop" color="7" fill="3" visible="yes" active="yes"/>
+<layer number="30" name="bStop" color="7" fill="6" visible="no" active="yes"/>
+<layer number="31" name="tCream" color="7" fill="4" visible="yes" active="yes"/>
+<layer number="32" name="bCream" color="7" fill="5" visible="no" active="yes"/>
+<layer number="33" name="tFinish" color="6" fill="3" visible="no" active="yes"/>
+<layer number="34" name="bFinish" color="6" fill="6" visible="no" active="yes"/>
+<layer number="35" name="tGlue" color="7" fill="4" visible="no" active="yes"/>
+<layer number="36" name="bGlue" color="7" fill="5" visible="no" active="yes"/>
+<layer number="37" name="tTest" color="7" fill="1" visible="no" active="yes"/>
+<layer number="38" name="bTest" color="7" fill="1" visible="no" active="yes"/>
+<layer number="39" name="tKeepout" color="4" fill="11" visible="yes" active="yes"/>
+<layer number="40" name="bKeepout" color="1" fill="11" visible="yes" active="yes"/>
+<layer number="41" name="tRestrict" color="4" fill="10" visible="yes" active="yes"/>
+<layer number="42" name="bRestrict" color="1" fill="10" visible="yes" active="yes"/>
+<layer number="43" name="vRestrict" color="2" fill="10" visible="yes" active="yes"/>
+<layer number="44" name="Drills" color="7" fill="1" visible="no" active="yes"/>
+<layer number="45" name="Holes" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="46" name="Milling" color="3" fill="1" visible="no" active="yes"/>
+<layer number="47" name="Measures" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="48" name="Document" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="49" name="Reference" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="50" name="dxf" color="7" fill="1" visible="no" active="no"/>
+<layer number="51" name="tDocu" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="52" name="bDocu" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="53" name="tGND_GNDA" color="7" fill="9" visible="no" active="no"/>
+<layer number="54" name="bGND_GNDA" color="7" fill="9" visible="no" active="no"/>
+<layer number="56" name="wert" color="7" fill="1" visible="no" active="no"/>
+<layer number="57" name="tCAD" color="7" fill="1" visible="no" active="no"/>
+<layer number="59" name="tCarbon" color="7" fill="1" visible="no" active="no"/>
+<layer number="60" name="bCarbon" color="7" fill="1" visible="no" active="no"/>
+<layer number="88" name="SimResults" color="9" fill="1" visible="no" active="no"/>
+<layer number="89" name="SimProbes" color="9" fill="1" visible="no" active="no"/>
+<layer number="90" name="Modules" color="5" fill="1" visible="no" active="no"/>
+<layer number="91" name="Nets" color="2" fill="1" visible="no" active="no"/>
+<layer number="92" name="Busses" color="1" fill="1" visible="no" active="no"/>
+<layer number="93" name="Pins" color="2" fill="1" visible="no" active="no"/>
+<layer number="94" name="Symbols" color="4" fill="1" visible="no" active="no"/>
+<layer number="95" name="Names" color="7" fill="1" visible="no" active="no"/>
+<layer number="96" name="Values" color="7" fill="1" visible="no" active="no"/>
+<layer number="97" name="Info" color="7" fill="1" visible="no" active="no"/>
+<layer number="98" name="Guide" color="6" fill="1" visible="no" active="no"/>
+<layer number="99" name="SpiceOrder" color="7" fill="1" visible="no" active="no"/>
+<layer number="100" name="Muster" color="7" fill="1" visible="no" active="no"/>
+<layer number="101" name="Patch_Top" color="7" fill="4" visible="no" active="yes"/>
+<layer number="102" name="Vscore" color="7" fill="1" visible="no" active="yes"/>
+<layer number="103" name="tMap" color="7" fill="1" visible="no" active="yes"/>
+<layer number="104" name="Name" color="7" fill="1" visible="no" active="yes"/>
+<layer number="105" name="tPlate" color="7" fill="1" visible="no" active="yes"/>
+<layer number="106" name="bPlate" color="7" fill="1" visible="no" active="yes"/>
+<layer number="107" name="Crop" color="7" fill="1" visible="no" active="yes"/>
+<layer number="108" name="tplace-old" color="7" fill="1" visible="no" active="yes"/>
+<layer number="109" name="ref-old" color="7" fill="1" visible="no" active="yes"/>
+<layer number="110" name="fp0" color="7" fill="1" visible="no" active="yes"/>
+<layer number="111" name="LPC17xx" color="7" fill="1" visible="no" active="yes"/>
+<layer number="112" name="tSilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="113" name="IDFDebug" color="7" fill="1" visible="no" active="yes"/>
+<layer number="114" name="Badge_Outline" color="7" fill="1" visible="no" active="yes"/>
+<layer number="115" name="ReferenceISLANDS" color="7" fill="1" visible="no" active="yes"/>
+<layer number="116" name="Patch_BOT" color="7" fill="4" visible="no" active="yes"/>
+<layer number="117" name="BACKMAAT1" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="118" name="Rect_Pads" color="7" fill="1" visible="no" active="yes"/>
+<layer number="119" name="KAP_TEKEN" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="120" name="KAP_MAAT1" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="121" name="_tsilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="122" name="_bsilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="123" name="tTestmark" color="7" fill="1" visible="no" active="yes"/>
+<layer number="124" name="bTestmark" color="7" fill="1" visible="no" active="yes"/>
+<layer number="125" name="_tNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="126" name="_bNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="127" name="_tValues" color="7" fill="1" visible="no" active="yes"/>
+<layer number="128" name="_bValues" color="7" fill="1" visible="no" active="yes"/>
+<layer number="129" name="Mask" color="7" fill="1" visible="no" active="yes"/>
+<layer number="130" name="SMDSTROOK" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="131" name="tAdjust" color="7" fill="1" visible="no" active="yes"/>
+<layer number="132" name="bAdjust" color="7" fill="1" visible="no" active="yes"/>
+<layer number="133" name="bottom_silk" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="144" name="Drill_legend" color="7" fill="1" visible="no" active="yes"/>
+<layer number="150" name="Notes" color="7" fill="1" visible="no" active="yes"/>
+<layer number="151" name="HeatSink" color="7" fill="1" visible="no" active="yes"/>
+<layer number="152" name="_bDocu" color="7" fill="1" visible="no" active="yes"/>
+<layer number="153" name="FabDoc1" color="7" fill="1" visible="no" active="yes"/>
+<layer number="154" name="FabDoc2" color="7" fill="1" visible="no" active="yes"/>
+<layer number="155" name="FabDoc3" color="7" fill="1" visible="no" active="yes"/>
+<layer number="199" name="Contour" color="7" fill="1" visible="no" active="yes"/>
+<layer number="200" name="200bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="201" name="201bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="202" name="202bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="203" name="203bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="204" name="204bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="205" name="205bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="206" name="206bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="207" name="207bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="208" name="208bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="209" name="209bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="210" name="210bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="211" name="211bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="212" name="212bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="213" name="213bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="214" name="214bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="215" name="215bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="216" name="216bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="217" name="217bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="218" name="218bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="219" name="219bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="220" name="220bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="221" name="221bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="222" name="222bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="223" name="223bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="224" name="224bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="225" name="225bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="226" name="226bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="227" name="227bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="228" name="228bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="229" name="229bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="230" name="230bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="231" name="231bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="232" name="Eagle3D_PG2" color="7" fill="1" visible="no" active="yes"/>
+<layer number="233" name="Eagle3D_PG3" color="7" fill="1" visible="no" active="yes"/>
+<layer number="248" name="Housing" color="7" fill="1" visible="no" active="yes"/>
+<layer number="249" name="Edge" color="7" fill="1" visible="no" active="yes"/>
+<layer number="250" name="Descript" color="7" fill="1" visible="no" active="no"/>
+<layer number="251" name="SMDround" color="7" fill="11" visible="no" active="no"/>
+<layer number="254" name="cooling" color="7" fill="1" visible="no" active="yes"/>
+<layer number="255" name="routoute" color="7" fill="1" visible="no" active="yes"/>
+</layers>
+<board>
+<plain>
+<wire x1="-12" y1="0" x2="35" y2="0" width="0" layer="20"/>
+<wire x1="35" y1="0" x2="35" y2="50" width="0" layer="20"/>
+<wire x1="35" y1="50" x2="-12" y2="50" width="0" layer="20"/>
+<wire x1="-12" y1="50" x2="-12" y2="0" width="0" layer="20"/>
+<dimension x1="4" y1="4" x2="17.5" y2="4" x3="10.75" y3="53" textsize="1.778" layer="47"/>
+<dimension x1="4" y1="4" x2="4" y2="46" x3="27.5" y3="25" textsize="1.778" layer="47"/>
+<circle x="4" y="4" radius="3" width="0.1524" layer="51"/>
+<circle x="17.5" y="4" radius="3" width="0.1524" layer="51"/>
+<circle x="4" y="46" radius="3" width="0.1524" layer="51"/>
+<circle x="17.5" y="46" radius="3" width="0.1524" layer="51"/>
+<dimension x1="4" y1="4" x2="-12" y2="4" x3="-4" y3="53" textsize="1.778" layer="47"/>
+<dimension x1="-12" y1="0" x2="-12" y2="50" x3="-19.5" y3="25" textsize="1.778" layer="47"/>
+<dimension x1="17.5" y1="4" x2="12" y2="4" x3="14.75" y3="51" textsize="1.778" layer="47"/>
+<dimension x1="17.5" y1="4" x2="17.5" y2="16" x3="-15.5" y3="10" textsize="1.778" layer="47"/>
+<dimension x1="22.5" y1="46" x2="22.5" y2="43" x3="30.5" y3="44.5" textsize="1.778" layer="47"/>
+<dimension x1="17.5" y1="41.5" x2="31.5" y2="41.5" x3="24.5" y3="58.5" textsize="1.778" layer="47"/>
+<hole x="4" y="46" drill="3.25"/>
+<hole x="17.5" y="46" drill="3.25"/>
+<hole x="4" y="4" drill="3.25"/>
+<hole x="17.5" y="4" drill="3.25"/>
+<dimension x1="-12" y1="0" x2="35" y2="0" x3="11.5" y3="-5.5" textsize="1.778" layer="47"/>
+<text x="-3" y="42.5" size="0.8128" layer="21" font="vector" rot="R90" align="center">RST</text>
+<text x="-4.7" y="2.5" size="0.8128" layer="21" font="vector" align="center">BUS</text>
+<text x="21.5" y="1.5" size="0.8128" layer="21" font="vector" rot="R90" align="center-left">mtm.cba.mit.edu</text>
+<text x="33.5" y="1.5" size="0.8128" layer="21" font="vector" rot="R90" align="center-left">filament-sensor 2021-10-27</text>
+</plain>
+<libraries>
+<library name="microcontrollers">
+<packages>
+<package name="TQFP-32-FAB">
+<wire x1="-3.55" y1="-3.55" x2="-3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="-3.55" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="-3.55" x2="-3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="3.55" x2="-3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="3.55" x2="-3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="3.25" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.25" x2="-3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.55" x2="-3.25" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.25" y1="-3.55" x2="3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="-3.55" x2="3.55" y2="-3.25" width="0.127" layer="21"/>
+<text x="-3.202909375" y="5.80526875" size="0.8135375" layer="25">&gt;NAME</text>
+<text x="-3.40625" y="-6.211390625" size="0.81429375" layer="27">&gt;VALUE</text>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="21"/>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="51"/>
+<smd name="1" x="-4.355" y="2.8" dx="1.25" dy="0.35" layer="1" roundness="25"/>
+<smd name="2" x="-4.18" y="2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="3" x="-4.18" y="1.2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="4" x="-4.18" y="0.4" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="5" x="-4.18" y="-0.4" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="6" x="-4.18" y="-1.2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="7" x="-4.18" y="-2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="8" x="-4.355" y="-2.8" dx="1.25" dy="0.35" layer="1" roundness="25"/>
+<smd name="9" x="-2.8" y="-4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="10" x="-2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="11" x="-1.2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="12" x="-0.4" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="13" x="0.4" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="14" x="1.2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="15" x="2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="16" x="2.8" y="-4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="17" x="4.355" y="-2.8" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="18" x="4.18" y="-2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="19" x="4.18" y="-1.2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="20" x="4.18" y="-0.4" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="21" x="4.18" y="0.4" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="22" x="4.18" y="1.2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="23" x="4.18" y="2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="24" x="4.355" y="2.8" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="25" x="2.8" y="4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="26" x="2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="27" x="1.2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="28" x="0.4" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="29" x="-0.4" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="30" x="-1.2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="31" x="-2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="32" x="-2.8" y="4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R270"/>
+</package>
+</packages>
+</library>
+<library name="SparkFun-Connectors" urn="urn:adsk.eagle:library:513">
+<description>&lt;h3&gt;SparkFun Connectors&lt;/h3&gt;
+This library contains electrically-functional connectors. 
+&lt;br&gt;
+&lt;br&gt;
+We've spent an enormous amount of time creating and checking these footprints and parts, but it is &lt;b&gt; the end user's responsibility&lt;/b&gt; to ensure correctness and suitablity for a given componet or application. 
+&lt;br&gt;
+&lt;br&gt;If you enjoy using this library, please buy one of our products at &lt;a href=" www.sparkfun.com"&gt;SparkFun.com&lt;/a&gt;.
+&lt;br&gt;
+&lt;br&gt;
+&lt;b&gt;Licensing:&lt;/b&gt; Creative Commons ShareAlike 4.0 International - https://creativecommons.org/licenses/by-sa/4.0/ 
+&lt;br&gt;
+&lt;br&gt;
+You are welcome to use this library for commercial purposes. For attribution, we ask that when you begin to sell your device using our footprint, you email us with a link to the product being sold. We want bragging rights that we helped (in a very small part) to create your 8th world wonder. We would like the opportunity to feature your device on our homepage.</description>
+<packages>
+<package name="2X5-PTH-1.27MM" urn="urn:adsk.eagle:footprint:37966/1" library_version="1">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 ARM Cortex Debug Connector (10-pin)&lt;/h3&gt;
+&lt;p&gt;tDoc (51) layer border represents maximum dimensions of plastic housing.&lt;/p&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:1.27mm&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”http://portal.fciconnect.com/Comergent//fci/drawing/20021111.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<pad name="8" x="1.27" y="0.635" drill="0.508" diameter="1"/>
+<pad name="6" x="0" y="0.635" drill="0.508" diameter="1"/>
+<pad name="4" x="-1.27" y="0.635" drill="0.508" diameter="1"/>
+<pad name="2" x="-2.54" y="0.635" drill="0.508" diameter="1"/>
+<pad name="10" x="2.54" y="0.635" drill="0.508" diameter="1"/>
+<pad name="7" x="1.27" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="5" x="0" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="3" x="-1.27" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="1" x="-2.54" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="9" x="2.54" y="-0.635" drill="0.508" diameter="1"/>
+<wire x1="-3.403" y1="-1.021" x2="-3.403" y2="-0.259" width="0.254" layer="21"/>
+<wire x1="3.175" y1="1.715" x2="-3.175" y2="1.715" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="1.715" x2="-3.175" y2="-1.715" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="-1.715" x2="3.175" y2="-1.715" width="0.127" layer="51"/>
+<wire x1="3.175" y1="-1.715" x2="3.175" y2="1.715" width="0.127" layer="51"/>
+<text x="-1.5748" y="1.9304" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-1.8288" y="-2.4638" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+</packages>
+<packages3d>
+<package3d name="2X5-PTH-1.27MM" urn="urn:adsk.eagle:package:38290/1" type="box" library_version="1">
+<description>Plated Through Hole - 2x5 ARM Cortex Debug Connector (10-pin)
+tDoc (51) layer border represents maximum dimensions of plastic housing.
+Specifications:
+Pin count:10
+Pin pitch:1.27mm
+
+Datasheet referenced for footprint
+Example device(s):
+CONN_05x2
+</description>
+<packageinstances>
+<packageinstance name="2X5-PTH-1.27MM"/>
+</packageinstances>
+</package3d>
+</packages3d>
+</library>
+<library name="passives">
+<packages>
+<package name="TACT-SWITCH-KMR6">
+<smd name="P$1" x="-2.05" y="0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$2" x="2.05" y="0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$3" x="-2.05" y="-0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$4" x="2.05" y="-0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<wire x1="-1.4" y1="0.8" x2="0" y2="0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="1.4" y2="0.8" width="0.127" layer="51"/>
+<wire x1="-1.4" y1="-0.8" x2="0" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="1.4" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="0" y2="0.6" width="0.127" layer="51"/>
+<wire x1="0" y1="0.6" x2="0.4" y2="-0.4" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="0" y2="-0.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="0.2" x2="-2.1" y2="-0.2" width="0.127" layer="51"/>
+<wire x1="2.1" y1="-0.2" x2="2.1" y2="0.2" width="0.127" layer="51"/>
+<wire x1="2.1" y1="1.4" x2="2.1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="2.1" y1="1.5" x2="1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="1.032" y1="1.5" x2="-2.1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="1.5" x2="-2.1" y2="1.4" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="-1.4" x2="-2.1" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="-1.5" x2="2.1" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="2.1" y1="-1.5" x2="2.1" y2="-1.4" width="0.127" layer="51"/>
+</package>
+<package name="0805">
+<smd name="1" x="-1" y="0" dx="0.8" dy="1.3" layer="1"/>
+<smd name="2" x="1" y="0" dx="0.8" dy="1.3" layer="1"/>
+<text x="-0.762" y="0.8255" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.016" y="-2.032" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-1" y1="-0.6" x2="1" y2="0.6" layer="51"/>
+<rectangle x1="-0.4" y1="-0.5" x2="0.4" y2="0.5" layer="21"/>
+</package>
+</packages>
+</library>
+<library name="comm">
+<packages>
+<package name="8-MSOP">
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="21"/>
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="1.5" y2="1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="-1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="-1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="1.5" y1="1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<text x="-2.5" y="-2" size="0.8128" layer="27" font="vector" align="top-left">&gt;VALUE</text>
+<text x="-2.5" y="2" size="0.8128" layer="25" font="vector">&gt;NAME</text>
+<smd name="1" x="-2.2" y="0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="2" x="-2.2" y="0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="3" x="-2.2" y="-0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="4" x="-2.2" y="-0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="5" x="2.2" y="-0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="6" x="2.2" y="-0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="7" x="2.2" y="0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="8" x="2.2" y="0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+</package>
+</packages>
+</library>
+<library name="connector">
+<packages>
+<package name="USB_MICRO_609-4613-1-ND">
+<smd name="HD0" x="-3.8" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD4" x="-3.1" y="2.55" dx="2.1" dy="1.6" layer="1"/>
+<smd name="HD5" x="3.1" y="2.55" dx="2.1" dy="1.6" layer="1"/>
+<smd name="D+" x="0" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="D-" x="-0.65" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="VBUS" x="-1.3" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="ID" x="0.65" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="GND" x="1.3" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<text x="4.9275" y="1.2125" size="0.6096" layer="27" font="vector" rot="R90">&gt;Value</text>
+<text x="-4.3925" y="1.13" size="0.6096" layer="25" font="vector" rot="R90">&gt;Name</text>
+<smd name="HD1" x="-1.05" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD2" x="1.05" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD3" x="3.8" y="0" dx="1.9" dy="1.8" layer="1"/>
+<wire x1="-4.7" y1="-1.45" x2="4.7" y2="-1.45" width="0.127" layer="51"/>
+<text x="0" y="-1.3" size="0.8128" layer="51" font="vector" align="bottom-center">\\ PCB Edge /</text>
+<wire x1="-3.9" y1="3" x2="-3.9" y2="-2.5" width="0.127" layer="51"/>
+<wire x1="-3.9" y1="-2.5" x2="3.9" y2="-2.5" width="0.127" layer="51"/>
+<wire x1="3.9" y1="-2.5" x2="3.9" y2="3" width="0.127" layer="51"/>
+<wire x1="3.9" y1="3" x2="-3.9" y2="3" width="0.127" layer="51"/>
+<wire x1="-3.9" y1="1.1" x2="-3.9" y2="1.5" width="0.127" layer="21"/>
+<wire x1="3.9" y1="1.1" x2="3.9" y2="1.5" width="0.127" layer="21"/>
+<wire x1="1.8" y1="3" x2="1.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-1.7" y1="3" x2="-1.8" y2="3" width="0.127" layer="21"/>
+<wire x1="4.4" y1="3" x2="4.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-4.4" y1="3" x2="-4.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-3.9" y1="3.6" x2="-3.9" y2="3.8" width="0.127" layer="21"/>
+<wire x1="3.9" y1="3.6" x2="3.9" y2="3.8" width="0.127" layer="21"/>
+</package>
+</packages>
+</library>
+<library name="power">
+<packages>
+<package name="SOT23-5">
+<description>&lt;b&gt;Small Outline Transistor&lt;/b&gt;, 5 lead</description>
+<wire x1="-1.544" y1="0.713" x2="1.544" y2="0.713" width="0.1524" layer="51"/>
+<wire x1="1.544" y1="0.713" x2="1.544" y2="-0.712" width="0.1524" layer="51"/>
+<wire x1="1.544" y1="-0.712" x2="-1.544" y2="-0.712" width="0.1524" layer="51"/>
+<wire x1="-1.544" y1="-0.712" x2="-1.544" y2="0.713" width="0.1524" layer="51"/>
+<smd name="5" x="-0.95" y="1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="4" x="0.95" y="1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="1" x="-0.95" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="2" x="0" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="3" x="0.95" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<text x="-1.778" y="-1.778" size="1.27" layer="25" ratio="10" rot="R90">&gt;NAME</text>
+<text x="3.048" y="-1.778" size="1.27" layer="27" ratio="10" rot="R90">&gt;VALUE</text>
+<rectangle x1="-1.1875" y1="0.7126" x2="-0.7125" y2="1.5439" layer="51"/>
+<rectangle x1="0.7125" y1="0.7126" x2="1.1875" y2="1.5439" layer="51"/>
+<rectangle x1="-1.1875" y1="-1.5437" x2="-0.7125" y2="-0.7124" layer="51"/>
+<rectangle x1="-0.2375" y1="-1.5437" x2="0.2375" y2="-0.7124" layer="51"/>
+<rectangle x1="0.7125" y1="-1.5437" x2="1.1875" y2="-0.7124" layer="51"/>
+<wire x1="-1.5" y1="-1.9" x2="-1.5" y2="-1.2" width="0.127" layer="21"/>
+</package>
+</packages>
+</library>
+<library name="SparkFun-Connectors">
+<description>&lt;h3&gt;SparkFun Connectors&lt;/h3&gt;
+This library contains electrically-functional connectors. 
+&lt;br&gt;
+&lt;br&gt;
+We've spent an enormous amount of time creating and checking these footprints and parts, but it is &lt;b&gt; the end user's responsibility&lt;/b&gt; to ensure correctness and suitablity for a given componet or application. 
+&lt;br&gt;
+&lt;br&gt;If you enjoy using this library, please buy one of our products at &lt;a href=" www.sparkfun.com"&gt;SparkFun.com&lt;/a&gt;.
+&lt;br&gt;
+&lt;br&gt;
+&lt;b&gt;Licensing:&lt;/b&gt; Creative Commons ShareAlike 4.0 International - https://creativecommons.org/licenses/by-sa/4.0/ 
+&lt;br&gt;
+&lt;br&gt;
+You are welcome to use this library for commercial purposes. For attribution, we ask that when you begin to sell your device using our footprint, you email us with a link to the product being sold. We want bragging rights that we helped (in a very small part) to create your 8th world wonder. We would like the opportunity to feature your device on our homepage.</description>
+<packages>
+<package name="2X5-SHROUDED_LOCK_LATCH">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header Locking Footprint&lt;/h3&gt;
+Holes are offset 0.005" from center, to hold pins in place during soldering. 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="16.1" x2="4.5" y2="-16.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="-16.1" x2="-4.5" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="-2.2" x2="-4.627" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="16.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="16.1" x2="4.4" y2="16.1" width="0.2032" layer="51"/>
+<wire x1="4.5" y1="-16.1" x2="-4.5" y2="-16.1" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.627" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<pad name="1" x="-1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<text x="-4.191" y="10.541" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.318" y="-11.049" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+<wire x1="-4.445" y1="16.16" x2="-4.445" y2="14.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="16.16" x2="-3.175" y2="16.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="16.16" x2="4.445" y2="16.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="16.16" x2="4.445" y2="14.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-14.89" x2="-4.445" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-16.16" x2="-3.175" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="-16.16" x2="4.445" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="-16.16" x2="4.445" y2="-14.89" width="0.127" layer="21"/>
+</package>
+</packages>
+</library>
+<library name="lights">
+<packages>
+<package name="LED0805">
+<smd name="1" x="-0.85" y="0" dx="1.1" dy="1" layer="1"/>
+<smd name="2" x="0.85" y="0" dx="1.1" dy="1" layer="1"/>
+<text x="-0.889" y="1.397" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.016" y="-2.413" size="1.016" layer="27" ratio="10">&gt;VALUE</text>
+<rectangle x1="-0.1999" y1="-0.3" x2="0.1999" y2="0.3" layer="35"/>
+<wire x1="-0.0778" y1="0.2818" x2="0.1278" y2="0" width="0.127" layer="21"/>
+<wire x1="0.1278" y1="0" x2="-0.0778" y2="-0.2818" width="0.127" layer="21"/>
+<wire x1="-0.0778" y1="0.2818" x2="-0.0778" y2="-0.2818" width="0.127" layer="21"/>
+</package>
+</packages>
+</library>
+<library name="sensor">
+<packages>
+<package name="TSSOP14">
+<description>&lt;b&gt;Thin Shrink Small Outline Plastic 14&lt;/b&gt;</description>
+<wire x1="-2.5146" y1="-2.0828" x2="2.5146" y2="-2.0828" width="0.1524" layer="51"/>
+<wire x1="2.5146" y1="2.0828" x2="2.5146" y2="-2.0828" width="0.1524" layer="51"/>
+<wire x1="2.5146" y1="2.0828" x2="-2.5146" y2="2.0828" width="0.1524" layer="51"/>
+<wire x1="-2.5146" y1="-2.0828" x2="-2.5146" y2="2.0828" width="0.1524" layer="51"/>
+<circle x="-3.0956" y="-1.6192" radius="0.3048" width="0.1524" layer="21"/>
+<smd name="1" x="-1.905" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="2" x="-1.27" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="3" x="-0.635" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="4" x="0" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="5" x="0.635" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="6" x="1.27" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="7" x="1.905" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="14" x="-1.905" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="13" x="-1.27" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="12" x="-0.635" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="11" x="0" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="10" x="0.635" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="9" x="1.27" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="8" x="1.905" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<text x="-2.8956" y="-2.0828" size="1.016" layer="25" ratio="10" rot="R90">&gt;NAME</text>
+<text x="3.8862" y="-2.0828" size="1.016" layer="27" ratio="10" rot="R90">&gt;VALUE</text>
+<rectangle x1="1.8034" y1="2.1082" x2="2.0066" y2="2.9464" layer="51"/>
+<rectangle x1="1.1684" y1="2.1082" x2="1.3716" y2="2.9464" layer="51"/>
+<rectangle x1="0.5334" y1="2.1082" x2="0.7366" y2="2.9464" layer="51"/>
+<rectangle x1="-0.1016" y1="2.1082" x2="0.1016" y2="2.9464" layer="51"/>
+<rectangle x1="-0.7366" y1="2.1082" x2="-0.5334" y2="2.9464" layer="51"/>
+<rectangle x1="-1.3716" y1="2.1082" x2="-1.1684" y2="2.9464" layer="51"/>
+<rectangle x1="-2.0066" y1="2.1082" x2="-1.8034" y2="2.9464" layer="51"/>
+<rectangle x1="-2.0066" y1="-2.921" x2="-1.8034" y2="-2.0828" layer="51"/>
+<rectangle x1="-1.3716" y1="-2.921" x2="-1.1684" y2="-2.0828" layer="51"/>
+<rectangle x1="-0.7366" y1="-2.921" x2="-0.5334" y2="-2.0828" layer="51"/>
+<rectangle x1="-0.1016" y1="-2.921" x2="0.1016" y2="-2.0828" layer="51"/>
+<rectangle x1="0.5334" y1="-2.921" x2="0.7366" y2="-2.0828" layer="51"/>
+<rectangle x1="1.1684" y1="-2.921" x2="1.3716" y2="-2.0828" layer="51"/>
+<rectangle x1="1.8034" y1="-2.921" x2="2.0066" y2="-2.0828" layer="51"/>
+</package>
+</packages>
+</library>
+<library name="sensor" urn="urn:adsk.wipprod:fs.file:vf.erO8XLirRvWBRSYvGu7cAQ">
+<packages>
+<package name="SOT23" library_version="1">
+<description>&lt;b&gt;SOT 23&lt;/b&gt;</description>
+<wire x1="1.4224" y1="0.6604" x2="1.4224" y2="-0.6604" width="0.1524" layer="51"/>
+<wire x1="1.4224" y1="-0.6604" x2="-1.4224" y2="-0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="-0.6604" x2="-1.4224" y2="0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="0.6604" x2="1.4224" y2="0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="-0.1524" x2="-1.4224" y2="0.6604" width="0.1524" layer="21"/>
+<wire x1="-1.4224" y1="0.6604" x2="-0.8636" y2="0.6604" width="0.1524" layer="21"/>
+<wire x1="1.4224" y1="0.6604" x2="1.4224" y2="-0.1524" width="0.1524" layer="21"/>
+<wire x1="0.8636" y1="0.6604" x2="1.4224" y2="0.6604" width="0.1524" layer="21"/>
+<smd name="3" x="0" y="1.1" dx="0.762" dy="1.016" layer="1"/>
+<smd name="2" x="0.95" y="-1.1" dx="0.762" dy="1.016" layer="1"/>
+<smd name="1" x="-0.95" y="-1.1" dx="0.762" dy="1.016" layer="1"/>
+<text x="-1.905" y="1.905" size="1.27" layer="25">&gt;NAME</text>
+<text x="-1.905" y="-3.175" size="1.27" layer="27">&gt;VALUE</text>
+<rectangle x1="-0.2286" y1="0.7112" x2="0.2286" y2="1.2954" layer="51"/>
+<rectangle x1="0.7112" y1="-1.2954" x2="1.1684" y2="-0.7112" layer="51"/>
+<rectangle x1="-1.1684" y1="-1.2954" x2="-0.7112" y2="-0.7112" layer="51"/>
+</package>
+</packages>
+</library>
+</libraries>
+<attributes>
+</attributes>
+<variantdefs>
+</variantdefs>
+<classes>
+<class number="0" name="default" width="0" drill="0">
+</class>
+</classes>
+<designrules name="default *">
+<param name="mdWireWire" value="6mil"/>
+<param name="mdWirePad" value="6mil"/>
+<param name="mdWireVia" value="6mil"/>
+<param name="mdPadPad" value="6mil"/>
+<param name="mdPadVia" value="6mil"/>
+<param name="mdViaVia" value="6mil"/>
+<param name="mdSmdPad" value="6mil"/>
+<param name="mdSmdVia" value="6mil"/>
+<param name="mdSmdSmd" value="6mil"/>
+<param name="mdCopperDimension" value="16mil"/>
+<param name="mdDrill" value="6mil"/>
+<param name="msWidth" value="6mil"/>
+<param name="msDrill" value="0.35mm"/>
+<param name="dpMaxLengthDifference" value="10mm"/>
+<param name="dpGapFactor" value="2.5"/>
+<description language="de">&lt;b&gt;EAGLE Design Rules&lt;/b&gt;
+&lt;p&gt;
+Die Standard-Design-Rules sind so gewählt, dass sie für 
+die meisten Anwendungen passen. Sollte ihre Platine 
+besondere Anforderungen haben, treffen Sie die erforderlichen
+Einstellungen hier und speichern die Design Rules unter 
+einem neuen Namen ab.</description>
+<description language="en">&lt;b&gt;EAGLE Design Rules&lt;/b&gt;
+&lt;p&gt;
+The default Design Rules have been set to cover
+a wide range of applications. Your particular design
+may have different requirements, so please make the
+necessary adjustments and save your customized
+design rules under a new name.</description>
+<param name="layerSetup" value="(1*16)"/>
+<param name="mtCopper" value="0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm 0.035mm"/>
+<param name="mtIsolate" value="1.5mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm"/>
+<param name="mdViaViaSameLayer" value="6mil"/>
+<param name="mnLayersViaInSmd" value="2"/>
+<param name="mdSmdStop" value="0mil"/>
+<param name="msMicroVia" value="9.99mm"/>
+<param name="msBlindViaRatio" value="0.5"/>
+<param name="rvPadTop" value="0.25"/>
+<param name="rvPadInner" value="0.25"/>
+<param name="rvPadBottom" value="0.25"/>
+<param name="rvViaOuter" value="0.25"/>
+<param name="rvViaInner" value="0.25"/>
+<param name="rvMicroViaOuter" value="0.25"/>
+<param name="rvMicroViaInner" value="0.25"/>
+<param name="rlMinPadTop" value="10mil"/>
+<param name="rlMaxPadTop" value="20mil"/>
+<param name="rlMinPadInner" value="10mil"/>
+<param name="rlMaxPadInner" value="20mil"/>
+<param name="rlMinPadBottom" value="10mil"/>
+<param name="rlMaxPadBottom" value="20mil"/>
+<param name="rlMinViaOuter" value="8mil"/>
+<param name="rlMaxViaOuter" value="20mil"/>
+<param name="rlMinViaInner" value="8mil"/>
+<param name="rlMaxViaInner" value="20mil"/>
+<param name="rlMinMicroViaOuter" value="4mil"/>
+<param name="rlMaxMicroViaOuter" value="20mil"/>
+<param name="rlMinMicroViaInner" value="4mil"/>
+<param name="rlMaxMicroViaInner" value="20mil"/>
+<param name="psTop" value="-1"/>
+<param name="psBottom" value="-1"/>
+<param name="psFirst" value="-1"/>
+<param name="psElongationLong" value="100"/>
+<param name="psElongationOffset" value="100"/>
+<param name="mvStopFrame" value="1"/>
+<param name="mvCreamFrame" value="0"/>
+<param name="mlMinStopFrame" value="4mil"/>
+<param name="mlMaxStopFrame" value="4mil"/>
+<param name="mlMinCreamFrame" value="0mil"/>
+<param name="mlMaxCreamFrame" value="0mil"/>
+<param name="mlViaStopLimit" value="40mil"/>
+<param name="srRoundness" value="0"/>
+<param name="srMinRoundness" value="0mil"/>
+<param name="srMaxRoundness" value="0mil"/>
+<param name="slThermalIsolate" value="10mil"/>
+<param name="slThermalsForVias" value="0"/>
+<param name="checkAngle" value="0"/>
+<param name="checkRestrict" value="1"/>
+<param name="checkStop" value="0"/>
+<param name="checkValues" value="0"/>
+<param name="checkNames" value="1"/>
+<param name="checkWireStubs" value="1"/>
+<param name="checkPolygonWidth" value="0"/>
+<param name="useDiameter" value="13"/>
+<param name="maxErrors" value="50"/>
+</designrules>
+<autorouter>
+<pass name="Default">
+<param name="RoutingGrid" value="50mil"/>
+<param name="AutoGrid" value="1"/>
+<param name="Efforts" value="0"/>
+<param name="TopRouterVariant" value="1"/>
+<param name="tpViaShape" value="round"/>
+<param name="PrefDir.1" value="a"/>
+<param name="PrefDir.2" value="0"/>
+<param name="PrefDir.3" value="0"/>
+<param name="PrefDir.4" value="0"/>
+<param name="PrefDir.5" value="0"/>
+<param name="PrefDir.6" value="0"/>
+<param name="PrefDir.7" value="0"/>
+<param name="PrefDir.8" value="0"/>
+<param name="PrefDir.9" value="0"/>
+<param name="PrefDir.10" value="0"/>
+<param name="PrefDir.11" value="0"/>
+<param name="PrefDir.12" value="0"/>
+<param name="PrefDir.13" value="0"/>
+<param name="PrefDir.14" value="0"/>
+<param name="PrefDir.15" value="0"/>
+<param name="PrefDir.16" value="a"/>
+<param name="cfVia" value="8"/>
+<param name="cfNonPref" value="5"/>
+<param name="cfChangeDir" value="2"/>
+<param name="cfOrthStep" value="2"/>
+<param name="cfDiagStep" value="3"/>
+<param name="cfExtdStep" value="0"/>
+<param name="cfBonusStep" value="1"/>
+<param name="cfMalusStep" value="1"/>
+<param name="cfPadImpact" value="4"/>
+<param name="cfSmdImpact" value="4"/>
+<param name="cfBusImpact" value="0"/>
+<param name="cfHugging" value="3"/>
+<param name="cfAvoid" value="4"/>
+<param name="cfPolygon" value="10"/>
+<param name="cfBase.1" value="0"/>
+<param name="cfBase.2" value="1"/>
+<param name="cfBase.3" value="1"/>
+<param name="cfBase.4" value="1"/>
+<param name="cfBase.5" value="1"/>
+<param name="cfBase.6" value="1"/>
+<param name="cfBase.7" value="1"/>
+<param name="cfBase.8" value="1"/>
+<param name="cfBase.9" value="1"/>
+<param name="cfBase.10" value="1"/>
+<param name="cfBase.11" value="1"/>
+<param name="cfBase.12" value="1"/>
+<param name="cfBase.13" value="1"/>
+<param name="cfBase.14" value="1"/>
+<param name="cfBase.15" value="1"/>
+<param name="cfBase.16" value="0"/>
+<param name="mnVias" value="20"/>
+<param name="mnSegments" value="9999"/>
+<param name="mnExtdSteps" value="9999"/>
+<param name="mnRipupLevel" value="10"/>
+<param name="mnRipupSteps" value="100"/>
+<param name="mnRipupTotal" value="100"/>
+</pass>
+<pass name="Follow-me" refer="Default" active="yes">
+</pass>
+<pass name="Busses" refer="Default" active="yes">
+<param name="cfNonPref" value="4"/>
+<param name="cfBusImpact" value="4"/>
+<param name="cfHugging" value="0"/>
+<param name="mnVias" value="0"/>
+</pass>
+<pass name="Route" refer="Default" active="yes">
+</pass>
+<pass name="Optimize1" refer="Default" active="yes">
+<param name="cfVia" value="99"/>
+<param name="cfExtdStep" value="10"/>
+<param name="cfHugging" value="1"/>
+<param name="mnExtdSteps" value="1"/>
+<param name="mnRipupLevel" value="0"/>
+</pass>
+<pass name="Optimize2" refer="Optimize1" active="yes">
+<param name="cfNonPref" value="0"/>
+<param name="cfChangeDir" value="6"/>
+<param name="cfExtdStep" value="0"/>
+<param name="cfBonusStep" value="2"/>
+<param name="cfMalusStep" value="2"/>
+<param name="cfPadImpact" value="2"/>
+<param name="cfSmdImpact" value="2"/>
+<param name="cfHugging" value="0"/>
+</pass>
+<pass name="Optimize3" refer="Optimize2" active="yes">
+<param name="cfChangeDir" value="8"/>
+<param name="cfPadImpact" value="0"/>
+<param name="cfSmdImpact" value="0"/>
+</pass>
+<pass name="Optimize4" refer="Optimize3" active="yes">
+<param name="cfChangeDir" value="25"/>
+</pass>
+</autorouter>
+<elements>
+<element name="U1" library="microcontrollers" package="TQFP-32-FAB" value="ATSAMD21E18A-AFFAB" x="7.5" y="37" smashed="yes" rot="R180">
+<attribute name="NAME" x="10.702909375" y="31.19473125" size="0.8135375" layer="25" rot="R180"/>
+</element>
+<element name="J1" library="SparkFun-Connectors" library_urn="urn:adsk.eagle:library:513" package="2X5-PTH-1.27MM" package3d_urn="urn:adsk.eagle:package:38290/1" value="CORTEX_DEBUG_PTH" x="7.5" y="25.5" smashed="yes" rot="MR0">
+<attribute name="NAME" x="9.0748" y="27.4304" size="0.6096" layer="26" font="vector" ratio="20" rot="MR0"/>
+<attribute name="VALUE" x="9.3288" y="23.0362" size="0.6096" layer="28" font="vector" ratio="20" rot="MR0"/>
+</element>
+<element name="S1" library="passives" package="TACT-SWITCH-KMR6" value="2-8X4-5_SWITCH" x="-5" y="42.5" smashed="yes" rot="R270"/>
+<element name="C1" library="passives" package="0805" value="0.1uF" x="8.5" y="30" smashed="yes" rot="R270">
+<attribute name="NAME" x="9.3255" y="30.762" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="8.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="TYPE" value="" x="8.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="9" y="27.9" size="0.8128" layer="21" font="vector" align="center"/>
+<attribute name="VOLTAGE" value="" x="8.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+</element>
+<element name="C2" library="passives" package="0805" value="1uF" x="6.5" y="30" smashed="yes" rot="R270">
+<attribute name="NAME" x="7.3255" y="30.762" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="6.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="TYPE" value="" x="6.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="6.1" y="27.9" size="0.8128" layer="21" font="vector" align="center"/>
+<attribute name="VOLTAGE" value="" x="6.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+</element>
+<element name="R1" library="passives" package="0805" value="10k" x="10.5" y="30" smashed="yes" rot="R270">
+<attribute name="NAME" x="11.3255" y="30.762" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="10.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="PRECISION" value="" x="10.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="11.9" y="27.9" size="0.8128" layer="21" font="vector" align="center"/>
+</element>
+<element name="U2" library="comm" package="8-MSOP" value="RS485-ISL83078EMSOP" x="-0.5" y="34.5" smashed="yes" rot="R270">
+<attribute name="NAME" x="1.5" y="37" size="0.8128" layer="25" font="vector" rot="R270"/>
+</element>
+<element name="U3" library="comm" package="8-MSOP" value="RS485-ISL83078EMSOP" x="-0.5" y="27" smashed="yes" rot="R270">
+<attribute name="NAME" x="1.5" y="29.5" size="0.8128" layer="25" font="vector" rot="R270"/>
+</element>
+<element name="X1" library="connector" package="USB_MICRO_609-4613-1-ND" value="USB" x="-10.584" y="44.266" smashed="yes" rot="R270">
+<attribute name="NAME" x="-9.454" y="48.6585" size="0.6096" layer="25" font="vector"/>
+</element>
+<element name="U4" library="power" package="SOT23-5" value="VREG-AP2112" x="-1" y="45.5" smashed="yes" rot="R180">
+<attribute name="NAME" x="0.778" y="47.278" size="1.27" layer="25" ratio="10" rot="R270"/>
+</element>
+<element name="C3" library="passives" package="0805" value="10uF" x="-1" y="48.5" smashed="yes">
+<attribute name="NAME" x="-1.762" y="49.3255" size="1.016" layer="25"/>
+<attribute name="PACKAGE" value="0805" x="-1" y="48.5" size="1.778" layer="27" display="off"/>
+<attribute name="TYPE" value="" x="-1" y="48.5" size="1.778" layer="27" display="off"/>
+<attribute name="VALUE" x="3" y="48.5" size="0.8128" layer="21" font="vector" align="center"/>
+<attribute name="VOLTAGE" value="" x="-1" y="48.5" size="1.778" layer="27" display="off"/>
+</element>
+<element name="C4" library="passives" package="0805" value="10uF" x="-1" y="42.5" smashed="yes" rot="R180">
+<attribute name="NAME" x="-0.238" y="41.6745" size="1.016" layer="25" rot="R180"/>
+<attribute name="PACKAGE" value="0805" x="-1" y="42.5" size="1.778" layer="27" rot="R180" display="off"/>
+<attribute name="TYPE" value="" x="-1" y="42.5" size="1.778" layer="27" rot="R180" display="off"/>
+<attribute name="VALUE" x="-1" y="41" size="0.8128" layer="21" font="vector" rot="R180" align="center"/>
+<attribute name="VOLTAGE" value="" x="-1" y="42.5" size="1.778" layer="27" rot="R180" display="off"/>
+</element>
+<element name="J2" library="SparkFun-Connectors" package="2X5-SHROUDED_LOCK_LATCH" value="" x="-7" y="23" smashed="yes" rot="R180">
+<attribute name="NAME" x="-2.809" y="12.459" size="0.6096" layer="25" font="vector" ratio="20" rot="R180"/>
+<attribute name="VALUE" x="-2.682" y="34.049" size="0.6096" layer="27" font="vector" ratio="20" rot="R180"/>
+</element>
+<element name="D1" library="lights" package="LED0805" value="LED0805" x="-5" y="46.1524" smashed="yes" rot="R180">
+<attribute name="NAME" x="-4.111" y="44.7554" size="1.016" layer="25" rot="R180"/>
+</element>
+<element name="R2" library="passives" package="0805" value="470R" x="-5" y="48" smashed="yes">
+<attribute name="NAME" x="-5.762" y="48.8255" size="1.016" layer="25"/>
+<attribute name="PACKAGE" value="0805" x="-5" y="48" size="1.778" layer="27" display="off"/>
+<attribute name="PRECISION" value="" x="-5" y="48" size="1.778" layer="27" display="off"/>
+<attribute name="VALUE" x="-5" y="49.3" size="0.8128" layer="21" font="vector" rot="R180" align="center"/>
+</element>
+<element name="D2" library="lights" package="LED0805" value="LED0805" x="-9.5" y="5.5" smashed="yes">
+<attribute name="NAME" x="-10.389" y="6.897" size="1.016" layer="25"/>
+</element>
+<element name="R3" library="passives" package="0805" value="470R" x="-6" y="5.5" smashed="yes" rot="R180">
+<attribute name="NAME" x="-5.238" y="4.6745" size="1.016" layer="25" rot="R180"/>
+<attribute name="PACKAGE" value="0805" x="-6" y="5.5" size="1.778" layer="27" rot="R180" display="off"/>
+<attribute name="PRECISION" value="" x="-6" y="5.5" size="1.778" layer="27" rot="R180" display="off"/>
+<attribute name="VALUE" x="-6" y="7.6" size="0.8128" layer="21" font="vector" rot="R180" align="center"/>
+</element>
+<element name="S2" library="passives" package="TACT-SWITCH-KMR6" value="2-8X4-5_SWITCH" x="-8.484" y="2.5" smashed="yes"/>
+<element name="R4" library="passives" package="0805" value="10k" x="4.5" y="30" smashed="yes" rot="R270">
+<attribute name="NAME" x="5.3255" y="30.762" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="4.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="PRECISION" value="" x="4.5" y="30" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="3.6" y="27.9" size="0.8128" layer="21" font="vector" align="center"/>
+</element>
+<element name="U5" library="sensor" package="TSSOP14" value="AS5047" x="12" y="16" smashed="yes">
+<attribute name="NAME" x="9.1044" y="13.9172" size="1.016" layer="25" ratio="10" rot="R90"/>
+</element>
+<element name="C5" library="passives" package="0805" value="0.1uF" x="10.5" y="44" smashed="yes" rot="R90">
+<attribute name="NAME" x="9.6745" y="43.238" size="1.016" layer="25" rot="R90"/>
+<attribute name="PACKAGE" value="0805" x="10.5" y="44" size="1.778" layer="27" rot="R90" display="off"/>
+<attribute name="TYPE" value="" x="10.5" y="44" size="1.778" layer="27" rot="R90" display="off"/>
+<attribute name="VALUE" x="12.032" y="43.984" size="0.8128" layer="21" font="vector" rot="R90" align="center"/>
+<attribute name="VOLTAGE" value="" x="10.5" y="44" size="1.778" layer="27" rot="R90" display="off"/>
+</element>
+<element name="U6" library="sensor" library_urn="urn:adsk.wipprod:fs.file:vf.erO8XLirRvWBRSYvGu7cAQ" package="SOT23" value="HALLSOT23" x="31.5" y="43" smashed="yes">
+<attribute name="NAME" x="29.595" y="44.905" size="1.27" layer="25"/>
+</element>
+<element name="C6" library="passives" package="0805" value="0.1uF" x="28.5" y="43" smashed="yes" rot="R270">
+<attribute name="NAME" x="29.3255" y="43.762" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="28.5" y="43" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="TYPE" value="" x="28.5" y="43" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="27" y="43" size="0.8128" layer="21" font="vector" rot="R270" align="center"/>
+<attribute name="VOLTAGE" value="" x="28.5" y="43" size="1.778" layer="27" rot="R270" display="off"/>
+</element>
+<element name="C7" library="passives" package="0805" value="0.1uF" x="11" y="20.5" smashed="yes">
+<attribute name="NAME" x="10.238" y="21.3255" size="1.016" layer="25"/>
+<attribute name="PACKAGE" value="0805" x="11" y="20.5" size="1.778" layer="27" display="off"/>
+<attribute name="TYPE" value="" x="11" y="20.5" size="1.778" layer="27" display="off"/>
+<attribute name="VALUE" x="11" y="21.9" size="0.8128" layer="21" font="vector" align="center"/>
+<attribute name="VOLTAGE" value="" x="11" y="20.5" size="1.778" layer="27" display="off"/>
+</element>
+<element name="C8" library="passives" package="0805" value="0.1uF" x="0.5" y="21.5" smashed="yes" rot="R270">
+<attribute name="NAME" x="1.3255" y="22.262" size="1.016" layer="25" rot="R270"/>
+<attribute name="PACKAGE" value="0805" x="0.5" y="21.5" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="TYPE" value="" x="0.5" y="21.5" size="1.778" layer="27" rot="R270" display="off"/>
+<attribute name="VALUE" x="-1" y="21.5" size="0.8128" layer="21" font="vector" rot="R270" align="center"/>
+<attribute name="VOLTAGE" value="" x="0.5" y="21.5" size="1.778" layer="27" rot="R270" display="off"/>
+</element>
+</elements>
+<signals>
+<signal name="GND">
+<contactref element="U1" pad="28"/>
+<contactref element="U1" pad="10"/>
+<contactref element="J1" pad="3"/>
+<contactref element="J1" pad="5"/>
+<contactref element="J1" pad="9"/>
+<contactref element="S1" pad="P$2"/>
+<contactref element="C2" pad="2"/>
+<contactref element="C1" pad="2"/>
+<contactref element="X1" pad="GND"/>
+<contactref element="C3" pad="2"/>
+<contactref element="U4" pad="2"/>
+<contactref element="C4" pad="2"/>
+<contactref element="J2" pad="5"/>
+<contactref element="J2" pad="6"/>
+<contactref element="U3" pad="5"/>
+<contactref element="U2" pad="5"/>
+<contactref element="R2" pad="1"/>
+<contactref element="R3" pad="1"/>
+<contactref element="S2" pad="P$4"/>
+<contactref element="C5" pad="2"/>
+<contactref element="U6" pad="3"/>
+<contactref element="C6" pad="1"/>
+<contactref element="U5" pad="13"/>
+<contactref element="C7" pad="1"/>
+<polygon width="0.2032" layer="16" spacing="1.27" pour="solid" thermals="no">
+<vertex x="-12.8984" y="-0.8984"/>
+<vertex x="36.3984" y="-0.8984"/>
+<vertex x="36.3984" y="50.8984"/>
+<vertex x="-12.8984" y="50.8984"/>
+</polygon>
+<via x="-9.1534" y="43" extent="1-16" drill="0.35"/>
+<wire x1="-1" y1="46.806" x2="-1" y2="47.5" width="0.4064" layer="1"/>
+<wire x1="-1" y1="47.5" x2="0" y2="48.5" width="0.4064" layer="1"/>
+<wire x1="0" y1="48.5" x2="1" y2="48.5" width="0.4064" layer="1"/>
+<via x="1" y="48.5" extent="1-16" drill="0.35"/>
+<wire x1="-2" y1="42.5" x2="-2.5" y2="42.5" width="0.4064" layer="1"/>
+<wire x1="-2.5" y1="42.5" x2="-3" y2="43" width="0.4064" layer="1"/>
+<via x="-3" y="43" extent="1-16" drill="0.35"/>
+<wire x1="-4.2" y1="40.45" x2="-3.45" y2="40.45" width="0.2032" layer="1"/>
+<wire x1="-3.45" y1="40.45" x2="-3" y2="40" width="0.2032" layer="1"/>
+<via x="-3" y="40" extent="1-16" drill="0.35"/>
+<wire x1="-6" y1="48" x2="-5" y2="48" width="0.2032" layer="1"/>
+<via x="-5" y="48" extent="1-16" drill="0.35"/>
+<wire x1="-1.475" y1="32.3" x2="-2.3" y2="32.3" width="0.2032" layer="1"/>
+<via x="-2.3" y="32.3" extent="1-16" drill="0.35"/>
+<wire x1="-1.475" y1="24.8" x2="-2.3" y2="24.8" width="0.2032" layer="1"/>
+<via x="-2.3" y="24.8" extent="1-16" drill="0.35"/>
+<wire x1="-5" y1="5.5" x2="-4" y2="5.5" width="0.2032" layer="1"/>
+<via x="-4" y="5.5" extent="1-16" drill="0.35"/>
+<wire x1="-6.434" y1="1.7" x2="-5.4" y2="1.7" width="0.2032" layer="1"/>
+<via x="-5.4" y="1.7" extent="1-16" drill="0.35"/>
+<wire x1="9.5" y1="41.18" x2="9.5" y2="44" width="0.2032" layer="1"/>
+<wire x1="9.5" y1="44" x2="10.5" y2="45" width="0.2032" layer="1"/>
+<wire x1="9.5" y1="41.18" x2="9.5" y2="39.6" width="0.2032" layer="1"/>
+<via x="9.5" y="39.6" extent="1-16" drill="0.35"/>
+<wire x1="7.1" y1="32.82" x2="7.1" y2="34" width="0.2032" layer="1"/>
+<wire x1="7.1" y1="34" x2="7.4" y2="34.3" width="0.2032" layer="1"/>
+<via x="7.4" y="34.3" extent="1-16" drill="0.35"/>
+<wire x1="6.5" y1="29" x2="7.5" y2="29" width="0.2032" layer="1"/>
+<wire x1="7.5" y1="29" x2="8.5" y2="29" width="0.2032" layer="1"/>
+<wire x1="7.5" y1="29" x2="7.5" y2="28.1" width="0.2032" layer="1"/>
+<via x="7.5" y="28.1" extent="1-16" drill="0.35"/>
+<wire x1="10" y1="20.5" x2="10" y2="21.8" width="0.2032" layer="1"/>
+<via x="10" y="21.8" extent="1-16" drill="0.35"/>
+<wire x1="10.7" y1="18.7478" x2="10.73" y2="18.7178" width="0.2032" layer="1"/>
+<wire x1="10" y1="20.5" x2="10.73" y2="19.77" width="0.2032" layer="1"/>
+<wire x1="10.73" y1="19.77" x2="10.73" y2="18.7178" width="0.2032" layer="1"/>
+<wire x1="-7.909" y1="42.966" x2="-9.1194" y2="42.966" width="0.2032" layer="1"/>
+<wire x1="-9.1194" y1="42.966" x2="-9.1534" y2="43" width="0.2032" layer="1"/>
+<wire x1="31.5" y1="44.1" x2="31.4" y2="44" width="0.2032" layer="1"/>
+<wire x1="31.4" y1="44" x2="28.5" y2="44" width="0.2032" layer="1"/>
+<wire x1="28.5" y1="44" x2="27" y2="44" width="0.2032" layer="1"/>
+<via x="27" y="44" extent="1-16" drill="0.35"/>
+<contactref element="C8" pad="2"/>
+<wire x1="0.5" y1="20.5" x2="0.5" y2="19.5" width="0.254" layer="1"/>
+<via x="0.5" y="19.5" extent="1-16" drill="0.35"/>
+</signal>
+<signal name="+3V3">
+<contactref element="J1" pad="1"/>
+<contactref element="C1" pad="1"/>
+<contactref element="U1" pad="9"/>
+<contactref element="U1" pad="30"/>
+<contactref element="R1" pad="2"/>
+<contactref element="U3" pad="8"/>
+<contactref element="U2" pad="8"/>
+<contactref element="C4" pad="1"/>
+<contactref element="U4" pad="5"/>
+<contactref element="R4" pad="2"/>
+<contactref element="C5" pad="1"/>
+<contactref element="U6" pad="1"/>
+<contactref element="C6" pad="2"/>
+<contactref element="U5" pad="12"/>
+<contactref element="C7" pad="2"/>
+<contactref element="U5" pad="11"/>
+<wire x1="-0.05" y1="44.194" x2="0" y2="44.144" width="0.4064" layer="1"/>
+<wire x1="0" y1="44.144" x2="0" y2="42.5" width="0.4064" layer="1"/>
+<wire x1="-2.4" y1="29.7" x2="-1.7" y2="30.4" width="0.254" layer="1"/>
+<wire x1="-1.7" y1="30.4" x2="-0.5" y2="30.4" width="0.254" layer="1"/>
+<wire x1="-0.5" y1="30.4" x2="0.475" y2="31.375" width="0.254" layer="1"/>
+<wire x1="0.475" y1="31.375" x2="0.475" y2="32.3" width="0.254" layer="1"/>
+<wire x1="0.475" y1="32.3" x2="0.475" y2="33.125" width="0.254" layer="1"/>
+<wire x1="0.475" y1="33.125" x2="-2.1" y2="35.7" width="0.254" layer="1"/>
+<via x="-0.99980625" y="42.19980625" extent="1-16" drill="0.35"/>
+<via x="-1.7" y="39.97980625" extent="1-16" drill="0.35"/>
+<wire x1="-2.1" y1="39.57980625" x2="-1.7" y2="39.97980625" width="0.254" layer="1"/>
+<wire x1="-1.7" y1="39.97980625" x2="-1.7" y2="41.4996125" width="0.254" layer="16"/>
+<wire x1="-1.7" y1="41.4996125" x2="-0.99980625" y2="42.19980625" width="0.254" layer="16"/>
+<wire x1="-2.1" y1="35.7" x2="-2.1" y2="39.57980625" width="0.254" layer="1"/>
+<wire x1="-0.99980625" y1="42.19980625" x2="-0.6996125" y2="42.5" width="0.254" layer="1"/>
+<wire x1="-0.6996125" y1="42.5" x2="0" y2="42.5" width="0.254" layer="1"/>
+<wire x1="10.3" y1="41.355" x2="10.3" y2="42.8" width="0.2032" layer="1"/>
+<wire x1="10.3" y1="42.8" x2="10.5" y2="43" width="0.2032" layer="1"/>
+<wire x1="8.7" y1="32.82" x2="8.7" y2="31.2" width="0.2032" layer="1"/>
+<wire x1="8.7" y1="31.2" x2="8.5" y2="31" width="0.2032" layer="1"/>
+<wire x1="10.5" y1="29" x2="8.6" y2="30.9" width="0.2032" layer="1"/>
+<wire x1="8.6" y1="30.9" x2="8.5" y2="31" width="0.2032" layer="1"/>
+<wire x1="8" y1="30.3" x2="8.6" y2="30.9" width="0.2032" layer="1"/>
+<wire x1="10.04" y1="24.865" x2="11.1" y2="25.925" width="0.2032" layer="16"/>
+<wire x1="11.1" y1="25.925" x2="11.1" y2="27" width="0.2032" layer="16"/>
+<wire x1="11.1" y1="27" x2="10.1" y2="28" width="0.2032" layer="16"/>
+<via x="10.1" y="28" extent="1-16" drill="0.35"/>
+<wire x1="10.1" y1="28" x2="10.5" y2="28.4" width="0.2032" layer="1"/>
+<wire x1="10.5" y1="28.4" x2="10.5" y2="29" width="0.2032" layer="1"/>
+<wire x1="10.04" y1="24.865" x2="12" y2="22.905" width="0.2032" layer="1"/>
+<wire x1="12" y1="22.905" x2="12" y2="20.5" width="0.2032" layer="1"/>
+<wire x1="12" y1="20.5" x2="12" y2="18.7178" width="0.2032" layer="1"/>
+<wire x1="11.365" y1="18.7178" x2="12" y2="18.7178" width="0.2032" layer="1"/>
+<wire x1="10.7" y1="43" x2="10.5" y2="43" width="0.2032" layer="1"/>
+<wire x1="11.5" y1="42" x2="10.5" y2="43" width="0.2032" layer="1"/>
+<wire x1="10.3" y1="41.355" x2="10.3" y2="36.2" width="0.2032" layer="1"/>
+<wire x1="10.3" y1="36.2" x2="8.7" y2="34.6" width="0.2032" layer="1"/>
+<wire x1="8.7" y1="34.6" x2="8.7" y2="32.82" width="0.2032" layer="1"/>
+<wire x1="0" y1="42.5" x2="5.6" y2="42.5" width="0.3048" layer="1"/>
+<wire x1="5.6" y1="42.5" x2="8.9" y2="45.8" width="0.3048" layer="1"/>
+<wire x1="8.9" y1="45.8" x2="11.2" y2="45.8" width="0.3048" layer="1"/>
+<wire x1="11.2" y1="45.8" x2="11.6" y2="45.4" width="0.3048" layer="1"/>
+<wire x1="11.6" y1="45.4" x2="11.6" y2="43.9" width="0.3048" layer="1"/>
+<wire x1="11.6" y1="43.9" x2="10.7" y2="43" width="0.3048" layer="1"/>
+<wire x1="4.5" y1="29" x2="5.1444" y2="29.6444" width="0.2032" layer="1"/>
+<wire x1="5.1444" y1="29.6444" x2="5.2473" y2="29.6444" width="0.2032" layer="1"/>
+<wire x1="5.9029" y1="30.3" x2="8" y2="30.3" width="0.2032" layer="1"/>
+<wire x1="5.2473" y1="29.6444" x2="5.9029" y2="30.3" width="0.2032" layer="1"/>
+<wire x1="-2.4" y1="27.1" x2="-2.4" y2="29.7" width="0.254" layer="1"/>
+<wire x1="-1.8" y1="26.5" x2="-2.4" y2="27.1" width="0.254" layer="1"/>
+<wire x1="-0.60915625" y1="26.5" x2="-1.8" y2="26.5" width="0.254" layer="1"/>
+<wire x1="-0.60915625" y1="26.5" x2="0.475" y2="25.41584375" width="0.254" layer="1"/>
+<wire x1="0.475" y1="25.41584375" x2="0.475" y2="24.8" width="0.254" layer="1"/>
+<wire x1="30.55" y1="41.9" x2="28.6" y2="41.9" width="0.2032" layer="1"/>
+<wire x1="28.6" y1="41.9" x2="28.5" y2="42" width="0.2032" layer="1"/>
+<wire x1="28.5" y1="42" x2="11.5" y2="42" width="0.2032" layer="1"/>
+<contactref element="C8" pad="1"/>
+<wire x1="0.475" y1="24.8" x2="0.5" y2="24.775" width="0.254" layer="1"/>
+<wire x1="0.5" y1="24.775" x2="0.5" y2="22.5" width="0.254" layer="1"/>
+</signal>
+<signal name="SWDIO">
+<contactref element="J1" pad="2"/>
+<contactref element="U1" pad="32"/>
+<wire x1="10.04" y1="26.135" x2="10.04" y2="26.44" width="0.2032" layer="1"/>
+<wire x1="10.04" y1="26.44" x2="11.8" y2="28.2" width="0.2032" layer="1"/>
+<wire x1="11.8" y1="28.2" x2="11.8" y2="31.1092125" width="0.2032" layer="1"/>
+<wire x1="11.8" y1="31.1092125" x2="11.2414125" y2="31.6678" width="0.2032" layer="1"/>
+<wire x1="11.2414125" y1="31.6678" x2="10.73813125" y2="31.6678" width="0.2032" layer="1"/>
+<wire x1="10.73813125" y1="31.6678" x2="10.3" y2="32.10593125" width="0.2032" layer="1"/>
+<wire x1="10.3" y1="32.10593125" x2="10.3" y2="32.645" width="0.2032" layer="1"/>
+</signal>
+<signal name="SWDCLK">
+<contactref element="J1" pad="4"/>
+<contactref element="R1" pad="1"/>
+<contactref element="U1" pad="31"/>
+<wire x1="9.5" y1="32.82" x2="9.5" y2="32" width="0.2032" layer="1"/>
+<wire x1="9.5" y1="32" x2="10.5" y2="31" width="0.2032" layer="1"/>
+<wire x1="10.5" y1="31" x2="11.404" y2="30.096" width="0.2032" layer="1"/>
+<wire x1="11.404" y1="30.096" x2="11.404" y2="28.404" width="0.2032" layer="1"/>
+<wire x1="11.404" y1="28.404" x2="10.2" y2="27.2" width="0.2032" layer="1"/>
+<wire x1="10.2" y1="27.2" x2="9.835" y2="27.2" width="0.2032" layer="1"/>
+<wire x1="9.835" y1="27.2" x2="8.77" y2="26.135" width="0.2032" layer="1"/>
+</signal>
+<signal name="RESET">
+<contactref element="S1" pad="P$4"/>
+<contactref element="U1" pad="26"/>
+<contactref element="J1" pad="10"/>
+<contactref element="R4" pad="1"/>
+<wire x1="4.96" y1="26.135" x2="3.5" y2="27.595" width="0.2032" layer="1"/>
+<wire x1="3.5" y1="27.595" x2="3.5" y2="30" width="0.2032" layer="1"/>
+<wire x1="3.5" y1="30" x2="4.45" y2="30.95" width="0.2032" layer="1"/>
+<wire x1="4.45" y1="30.95" x2="4.5" y2="31" width="0.2032" layer="1"/>
+<wire x1="5.5" y1="32.82" x2="5.5" y2="32" width="0.2032" layer="1"/>
+<wire x1="5.5" y1="32" x2="4.5" y2="31" width="0.2032" layer="1"/>
+<via x="-3.8" y="27.7" extent="1-16" drill="0.35"/>
+<wire x1="-4.3838" y1="28.5850125" x2="-4.3838" y2="28.2838" width="0.2032" layer="1"/>
+<wire x1="-4.3838" y1="28.2838" x2="-3.8" y2="27.7" width="0.2032" layer="1"/>
+<wire x1="-5.8" y1="40.45" x2="-5.8" y2="30.0012125" width="0.2032" layer="1"/>
+<wire x1="-5.8" y1="30.0012125" x2="-4.3838" y2="28.5850125" width="0.2032" layer="1"/>
+<wire x1="-3.8" y1="27.7" x2="-1.7" y2="27.7" width="0.2032" layer="16"/>
+<via x="-1.7" y="27.7" extent="1-16" drill="0.35"/>
+<wire x1="-1.7" y1="27.7" x2="1.8" y2="27.7" width="0.2032" layer="1"/>
+<wire x1="2.8" y1="26.7" x2="2.8" y2="26.1" width="0.2032" layer="1"/>
+<wire x1="1.8" y1="27.7" x2="2.8" y2="26.7" width="0.2032" layer="1"/>
+<via x="2.8" y="26.1" extent="1-16" drill="0.35"/>
+<wire x1="2.8" y1="26.1" x2="4.925" y2="26.1" width="0.2032" layer="16"/>
+<wire x1="4.925" y1="26.1" x2="4.96" y2="26.135" width="0.2032" layer="16"/>
+</signal>
+<signal name="N$5">
+<contactref element="U1" pad="29"/>
+<contactref element="C2" pad="1"/>
+<wire x1="7.9" y1="32.82" x2="7.9" y2="32" width="0.2032" layer="1"/>
+<wire x1="7.9" y1="32" x2="6.9" y2="31" width="0.2032" layer="1"/>
+<wire x1="6.9" y1="31" x2="6.5" y2="31" width="0.2032" layer="1"/>
+</signal>
+<signal name="+5V">
+<contactref element="X1" pad="VBUS"/>
+<contactref element="U4" pad="3"/>
+<contactref element="C3" pad="1"/>
+<contactref element="U4" pad="1"/>
+<contactref element="J2" pad="9"/>
+<contactref element="J2" pad="2"/>
+<wire x1="-7.1896" y1="45.566" x2="-7.909" y2="45.566" width="0.4064" layer="1"/>
+<wire x1="-7.1896" y1="45.566" x2="-6.7556" y2="46" width="0.4064" layer="1"/>
+<wire x1="-6.7556" y1="48.79729375" x2="-6.54729375" y2="49.0056" width="0.4064" layer="1"/>
+<wire x1="-6.7556" y1="46" x2="-6.7556" y2="48.79729375" width="0.4064" layer="1"/>
+<wire x1="-6.54729375" y1="49.0056" x2="-2.5056" y2="49.0056" width="0.4064" layer="1"/>
+<wire x1="-2.5056" y1="49.0056" x2="-2" y2="48.5" width="0.4064" layer="1"/>
+<polygon width="0.4064" layer="1" spacing="1.27" pour="solid" thermals="no">
+<vertex x="-4.2968" y="42.7032"/>
+<vertex x="5.2968" y="42.7032"/>
+<vertex x="5.2968" y="50.2968"/>
+<vertex x="-4.2968" y="50.2968"/>
+</polygon>
+<via x="-9.1534" y="45.5" extent="1-16" drill="0.35"/>
+<wire x1="-5.603" y1="28.08" x2="-5.78" y2="28.08" width="0.4064" layer="16"/>
+<wire x1="-5.78" y1="28.08" x2="-11.2" y2="33.5" width="0.4064" layer="16"/>
+<wire x1="-11.2" y1="33.5" x2="-11.2" y2="20.723" width="0.4064" layer="16"/>
+<wire x1="-11.2" y1="20.723" x2="-8.397" y2="17.92" width="0.4064" layer="16"/>
+<wire x1="-7.909" y1="45.566" x2="-9.0874" y2="45.566" width="0.4064" layer="1"/>
+<wire x1="-9.0874" y1="45.566" x2="-9.1534" y2="45.5" width="0.4064" layer="1"/>
+<wire x1="-11.2" y1="33.5" x2="-11.2" y2="43.4534" width="0.4064" layer="16"/>
+<wire x1="-11.2" y1="43.4534" x2="-9.1534" y2="45.5" width="0.4064" layer="16"/>
+</signal>
+<signal name="USBDM">
+<contactref element="X1" pad="D-"/>
+<contactref element="U1" pad="23"/>
+<wire x1="-6.98" y1="44.4241125" x2="-6.98" y2="44.5712125" width="0.2032" layer="1"/>
+<wire x1="-7.3247875" y1="44.916" x2="-7.909" y2="44.916" width="0.2032" layer="1"/>
+<wire x1="-6.98" y1="44.5712125" x2="-7.3247875" y2="44.916" width="0.2032" layer="1"/>
+<wire x1="-6.6244" y1="44.0685125" x2="-6.98" y2="44.4241125" width="0.2032" layer="1"/>
+<wire x1="-6.6244" y1="43.1244" x2="-6.6244" y2="44.0685125" width="0.2032" layer="1"/>
+<wire x1="-5.7" y1="42.2" x2="-6.6244" y2="43.1244" width="0.2032" layer="1"/>
+<wire x1="-3.6378875" y1="42.2" x2="-5.7" y2="42.2" width="0.2032" layer="1"/>
+<wire x1="6.2322" y1="38.5707" x2="3.83529375" y2="40.96760625" width="0.2032" layer="1"/>
+<wire x1="3.83529375" y1="40.96760625" x2="-2.40549375" y2="40.96760625" width="0.2032" layer="1"/>
+<wire x1="-2.40549375" y1="40.96760625" x2="-3.6378875" y2="42.2" width="0.2032" layer="1"/>
+<wire x1="5.33813125" y1="34.79768125" x2="5.86186875" y2="34.79768125" width="0.2032" layer="1"/>
+<wire x1="5.86186875" y1="34.79768125" x2="6.2322" y2="35.1680125" width="0.2032" layer="1"/>
+<wire x1="6.2322" y1="35.1680125" x2="6.2322" y2="38.5707" width="0.2032" layer="1"/>
+<wire x1="3.32" y1="35" x2="4.50593125" y2="35" width="0.2032" layer="1"/>
+<wire x1="4.50593125" y1="35" x2="4.53813125" y2="35.0322" width="0.2032" layer="1"/>
+<wire x1="4.53813125" y1="35.0322" x2="5.1036125" y2="35.0322" width="0.2032" layer="1"/>
+<wire x1="5.1036125" y1="35.0322" x2="5.33813125" y2="34.79768125" width="0.2032" layer="1"/>
+</signal>
+<signal name="USBDP">
+<contactref element="U1" pad="24"/>
+<contactref element="X1" pad="D+"/>
+<wire x1="-7.905" y1="44.27" x2="-7.3287875" y2="44.27" width="0.2032" layer="1"/>
+<wire x1="-7.909" y1="44.266" x2="-7.905" y2="44.27" width="0.2032" layer="1"/>
+<wire x1="-7.3287875" y1="44.27" x2="-6.98" y2="43.9212125" width="0.2032" layer="1"/>
+<wire x1="-6.98" y1="43.9212125" x2="-6.98" y2="42.9771" width="0.2032" layer="1"/>
+<wire x1="-6.98" y1="42.9771" x2="-5.8473" y2="41.8444" width="0.2032" layer="1"/>
+<wire x1="-5.8473" y1="41.8444" x2="-3.7851875" y2="41.8444" width="0.2032" layer="1"/>
+<wire x1="5.6" y1="35.42988125" x2="5.6" y2="35.2" width="0.2032" layer="16"/>
+<wire x1="5.6" y1="35.2" x2="4.8" y2="34.4" width="0.2032" layer="16"/>
+<via x="4.8" y="34.4" extent="1-16" drill="0.35"/>
+<wire x1="4.8" y1="34.4" x2="4.6" y2="34.2" width="0.2032" layer="1"/>
+<wire x1="4.6" y1="34.2" x2="3.145" y2="34.2" width="0.2032" layer="1"/>
+<wire x1="3.68799375" y1="40.61200625" x2="5.8" y2="38.5" width="0.2032" layer="1"/>
+<wire x1="-3.7851875" y1="41.8444" x2="-2.55279375" y2="40.61200625" width="0.2032" layer="1"/>
+<wire x1="-2.55279375" y1="40.61200625" x2="3.68799375" y2="40.61200625" width="0.2032" layer="1"/>
+<via x="5.6" y="35.42988125" extent="1-16" drill="0.35"/>
+<wire x1="5.8" y1="38.5" x2="5.8" y2="35.62988125" width="0.2032" layer="1"/>
+<wire x1="5.8" y1="35.62988125" x2="5.6" y2="35.42988125" width="0.2032" layer="1"/>
+</signal>
+<signal name="Y">
+<contactref element="J2" pad="7"/>
+<contactref element="U2" pad="6"/>
+<wire x1="-0.825" y1="32.3" x2="-0.825" y2="31.375" width="0.2032" layer="1"/>
+<wire x1="-0.825" y1="31.375" x2="-1.2" y2="31" width="0.2032" layer="1"/>
+<wire x1="-1.2" y1="31" x2="-2" y2="31" width="0.2032" layer="1"/>
+<wire x1="-2" y1="31" x2="-2.8" y2="30.2" width="0.2032" layer="1"/>
+<wire x1="-2.8" y1="30.2" x2="-2.8" y2="26.9971" width="0.2032" layer="1"/>
+<wire x1="-2.8" y1="26.9971" x2="-4.2571" y2="25.54" width="0.2032" layer="1"/>
+<wire x1="-4.2571" y1="25.54" x2="-5.603" y2="25.54" width="0.2032" layer="1"/>
+</signal>
+<signal name="B">
+<contactref element="J2" pad="3"/>
+<contactref element="U3" pad="7"/>
+<wire x1="-5.603" y1="20.46" x2="-4.763" y2="21.3" width="0.2032" layer="1"/>
+<wire x1="-4.763" y1="21.3" x2="-2.5" y2="21.3" width="0.2032" layer="1"/>
+<wire x1="-2.5" y1="21.3" x2="-0.175" y2="23.625" width="0.2032" layer="1"/>
+<wire x1="-0.175" y1="23.625" x2="-0.175" y2="24.8" width="0.2032" layer="1"/>
+</signal>
+<signal name="A">
+<contactref element="J2" pad="4"/>
+<contactref element="U3" pad="6"/>
+<wire x1="-0.825" y1="24.8" x2="-0.825" y2="23.675" width="0.2032" layer="1"/>
+<wire x1="-0.825" y1="23.675" x2="-2.8" y2="21.7" width="0.2032" layer="1"/>
+<wire x1="-2.8" y1="21.7" x2="-7.157" y2="21.7" width="0.2032" layer="1"/>
+<wire x1="-7.157" y1="21.7" x2="-8.397" y2="20.46" width="0.2032" layer="1"/>
+</signal>
+<signal name="Z">
+<contactref element="J2" pad="8"/>
+<contactref element="U2" pad="7"/>
+<wire x1="-8.397" y1="25.54" x2="-7.137" y2="26.8" width="0.2032" layer="1"/>
+<wire x1="-7.137" y1="26.8" x2="-3.5" y2="26.8" width="0.2032" layer="1"/>
+<wire x1="-2.2" y1="33.5" x2="-0.5" y2="33.5" width="0.2032" layer="1"/>
+<wire x1="-0.5" y1="33.5" x2="-0.175" y2="33.175" width="0.2032" layer="1"/>
+<wire x1="-0.175" y1="33.175" x2="-0.175" y2="32.3" width="0.2032" layer="1"/>
+<wire x1="-3.5" y1="26.8" x2="-3.1556" y2="27.1444" width="0.2032" layer="1"/>
+<wire x1="-3.1556" y1="27.1444" x2="-3.1556" y2="32.5444" width="0.2032" layer="1"/>
+<wire x1="-3.1556" y1="32.5444" x2="-2.2" y2="33.5" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_RE">
+<contactref element="U3" pad="2"/>
+<contactref element="U1" pad="19"/>
+<wire x1="3.32" y1="38.2" x2="2.1" y2="38.2" width="0.2032" layer="1"/>
+<wire x1="-0.175" y1="30.1279" x2="-0.175" y2="29.2" width="0.2032" layer="1"/>
+<wire x1="1.1444" y1="31.4473" x2="-0.175" y2="30.1279" width="0.2032" layer="1"/>
+<wire x1="2.1" y1="38.2" x2="1.1444" y2="37.2444" width="0.2032" layer="1"/>
+<wire x1="1.1444" y1="37.2444" x2="1.1444" y2="31.4473" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_RX">
+<contactref element="U3" pad="1"/>
+<contactref element="U1" pad="20"/>
+<wire x1="0.475" y1="29.2" x2="0.475" y2="30.275" width="0.2032" layer="1"/>
+<wire x1="0.475" y1="30.275" x2="1.5" y2="31.3" width="0.2032" layer="1"/>
+<wire x1="1.5" y1="31.3" x2="1.5" y2="37" width="0.2032" layer="1"/>
+<wire x1="1.5" y1="37" x2="1.9" y2="37.4" width="0.2032" layer="1"/>
+<wire x1="1.9" y1="37.4" x2="3.32" y2="37.4" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_TX">
+<contactref element="U2" pad="4"/>
+<contactref element="U1" pad="17"/>
+<wire x1="0.2" y1="39.8" x2="3.145" y2="39.8" width="0.2032" layer="1"/>
+<wire x1="0.2" y1="39.8" x2="-1.475" y2="38.125" width="0.2032" layer="1"/>
+<wire x1="-1.475" y1="38.125" x2="-1.475" y2="36.7" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_DE">
+<contactref element="U2" pad="3"/>
+<contactref element="U1" pad="18"/>
+<wire x1="3.32" y1="39" x2="0.1" y2="39" width="0.2032" layer="1"/>
+<wire x1="0.1" y1="39" x2="-0.825" y2="38.075" width="0.2032" layer="1"/>
+<wire x1="-0.825" y1="38.075" x2="-0.825" y2="36.7" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_L">
+<contactref element="U1" pad="21"/>
+<contactref element="D2" pad="1"/>
+<wire x1="3.32" y1="36.6" x2="2.2" y2="36.6" width="0.2032" layer="1"/>
+<wire x1="-10.35" y1="6.55" x2="-10.35" y2="5.5" width="0.2032" layer="1"/>
+<wire x1="2.2" y1="36.6" x2="2.1678" y2="36.5678" width="0.2032" layer="1"/>
+<wire x1="3.4322" y1="20.3322" x2="-10.35" y2="6.55" width="0.2032" layer="1"/>
+<wire x1="2.1678" y1="36.5678" x2="2.1678" y2="27.8351" width="0.2032" layer="1"/>
+<wire x1="2.1678" y1="27.8351" x2="3.4322" y2="26.5707" width="0.2032" layer="1"/>
+<wire x1="3.4322" y1="26.5707" x2="3.4322" y2="20.3322" width="0.2032" layer="1"/>
+</signal>
+<signal name="LIGHT">
+<contactref element="U1" pad="22"/>
+<contactref element="D1" pad="1"/>
+<wire x1="-4.15" y1="46.1524" x2="-5" y2="45.3024" width="0.2032" layer="1"/>
+<wire x1="-4.8052125" y1="43.846" x2="-4.746" y2="43.846" width="0.2032" layer="1"/>
+<wire x1="-4.746" y1="43.846" x2="-2.654" y2="41.754" width="0.2032" layer="1"/>
+<wire x1="-5" y1="45.3024" x2="-5" y2="44.0407875" width="0.2032" layer="1"/>
+<wire x1="-5" y1="44.0407875" x2="-4.8052125" y2="43.846" width="0.2032" layer="1"/>
+<wire x1="-2.654" y1="41.754" x2="-2.654" y2="41.7447875" width="0.2032" layer="1"/>
+<wire x1="-2.654" y1="41.7447875" x2="-2.3092125" y2="41.4" width="0.2032" layer="1"/>
+<via x="3.9944" y="41.7" extent="1-16" drill="0.35"/>
+<wire x1="-2.3092125" y1="41.4" x2="3.6944" y2="41.4" width="0.2032" layer="1"/>
+<wire x1="3.6944" y1="41.4" x2="3.9944" y2="41.7" width="0.2032" layer="1"/>
+<wire x1="3.9944" y1="41.7" x2="4.5" y2="41.1944" width="0.2032" layer="16"/>
+<via x="4.6506" y="38.7" extent="1-16" drill="0.35"/>
+<wire x1="4.5" y1="41.1944" x2="4.5" y2="38.8506" width="0.2032" layer="16"/>
+<wire x1="4.5" y1="38.8506" x2="4.6506" y2="38.7" width="0.2032" layer="16"/>
+<wire x1="4.6506" y1="38.7" x2="4.6506" y2="36.32201875" width="0.2032" layer="1"/>
+<wire x1="4.6506" y1="36.32201875" x2="4.12858125" y2="35.8" width="0.2032" layer="1"/>
+<wire x1="3.32" y1="35.8" x2="4.12858125" y2="35.8" width="0.2032" layer="1"/>
+</signal>
+<signal name="UCBUS_B">
+<contactref element="U1" pad="27"/>
+<contactref element="S2" pad="P$2"/>
+<wire x1="-4.6" y1="3.3" x2="-6.434" y2="3.3" width="0.2032" layer="1"/>
+<wire x1="6.3" y1="32.82" x2="6.3" y2="32.2092125" width="0.2032" layer="1"/>
+<wire x1="6.3" y1="32.2092125" x2="5.404" y2="31.3132125" width="0.2032" layer="1"/>
+<wire x1="5.404" y1="31.3132125" x2="5.404" y2="30.304" width="0.2032" layer="1"/>
+<wire x1="5.404" y1="30.304" x2="5.1" y2="30" width="0.2032" layer="1"/>
+<wire x1="5.1" y1="30" x2="4.5" y2="30" width="0.2032" layer="1"/>
+<via x="4.5" y="30" extent="1-16" drill="0.35"/>
+<wire x1="4.5" y1="30" x2="3" y2="30" width="0.2032" layer="16"/>
+<wire x1="3" y1="30" x2="2.8" y2="29.8" width="0.2032" layer="16"/>
+<via x="2.8" y="29.8" extent="1-16" drill="0.35"/>
+<wire x1="2.8" y1="29.8" x2="3.1" y2="29.5" width="0.2032" layer="1"/>
+<wire x1="3.7878" y1="11.6878" x2="-4.6" y2="3.3" width="0.2032" layer="1"/>
+<wire x1="3.1" y1="29.5" x2="3.1" y2="27.4058" width="0.2032" layer="1"/>
+<wire x1="3.1" y1="27.4058" x2="3.7878" y2="26.718" width="0.2032" layer="1"/>
+<wire x1="3.7878" y1="26.718" x2="3.7878" y2="11.6878" width="0.2032" layer="1"/>
+</signal>
+<signal name="N$1">
+<contactref element="D1" pad="2"/>
+<contactref element="R2" pad="2"/>
+<wire x1="-4" y1="48" x2="-5.8476" y2="46.1524" width="0.2032" layer="1"/>
+<wire x1="-5.8476" y1="46.1524" x2="-5.85" y2="46.1524" width="0.2032" layer="1"/>
+</signal>
+<signal name="N$2">
+<contactref element="D2" pad="2"/>
+<contactref element="R3" pad="2"/>
+<wire x1="-8.65" y1="5.5" x2="-7" y2="5.5" width="0.2032" layer="1"/>
+</signal>
+<signal name="HALL">
+<contactref element="U1" pad="3"/>
+<contactref element="U6" pad="2"/>
+<wire x1="32.45" y1="41.9" x2="30.35" y2="39.8" width="0.2032" layer="1"/>
+<wire x1="12.85958125" y1="35.771" x2="11.709" y2="35.771" width="0.2032" layer="1"/>
+<wire x1="11.709" y1="35.771" x2="11.68" y2="35.8" width="0.2032" layer="1"/>
+<wire x1="30.35" y1="39.8" x2="16.88858125" y2="39.8" width="0.2032" layer="1"/>
+<wire x1="16.88858125" y1="39.8" x2="12.85958125" y2="35.771" width="0.2032" layer="1"/>
+</signal>
+<signal name="0-0-MOSI">
+<contactref element="U1" pad="11"/>
+<contactref element="U5" pad="4"/>
+<wire x1="8.7" y1="41.18" x2="8.7" y2="37.489575" width="0.2032" layer="1"/>
+<wire x1="12.15560625" y1="34.1266" x2="10.8556125" y2="34.1266" width="0.2032" layer="16"/>
+<wire x1="16.6" y1="17.3" x2="16.6" y2="29.9025375" width="0.2032" layer="1"/>
+<via x="12.892375" y="33.4898375" extent="1-16" drill="0.35"/>
+<wire x1="12.892375" y1="33.4898375" x2="12.79236875" y2="33.4898375" width="0.2032" layer="16"/>
+<wire x1="16.6" y1="29.9025375" x2="13.0127" y2="33.4898375" width="0.2032" layer="1"/>
+<wire x1="13.0127" y1="33.4898375" x2="12.892375" y2="33.4898375" width="0.2032" layer="1"/>
+<wire x1="12.79236875" y1="33.4898375" x2="12.15560625" y2="34.1266" width="0.2032" layer="16"/>
+<wire x1="13.6" y1="14.3" x2="16.6" y2="17.3" width="0.2032" layer="1"/>
+<wire x1="12.3" y1="14.3" x2="13.6" y2="14.3" width="0.2032" layer="1"/>
+<wire x1="12.3" y1="14.3" x2="12" y2="14" width="0.2032" layer="1"/>
+<wire x1="12" y1="14" x2="12" y2="13.2822" width="0.2032" layer="1"/>
+<via x="9.5991875" y="36.5903875" extent="1-16" drill="0.35"/>
+<wire x1="10.8556125" y1="34.1266" x2="9.5991875" y2="35.383025" width="0.2032" layer="16"/>
+<wire x1="9.5991875" y1="35.383025" x2="9.5991875" y2="36.5903875" width="0.2032" layer="16"/>
+<wire x1="8.7" y1="37.489575" x2="9.5991875" y2="36.5903875" width="0.2032" layer="1"/>
+</signal>
+<signal name="0-1-CLK">
+<contactref element="U1" pad="12"/>
+<contactref element="U5" pad="2"/>
+<wire x1="7.9" y1="41.18" x2="7.9" y2="39.7058" width="0.2032" layer="1"/>
+<via x="8.63966875" y="36.6558375" extent="1-16" drill="0.35"/>
+<via x="11.8937375" y="33.4944" extent="1-16" drill="0.35"/>
+<wire x1="7.9" y1="39.7058" x2="8.1444" y2="39.4614" width="0.2032" layer="1"/>
+<wire x1="8.1444" y1="39.4614" x2="8.1444" y2="37.15110625" width="0.2032" layer="1"/>
+<wire x1="8.1444" y1="37.15110625" x2="8.63966875" y2="36.6558375" width="0.2032" layer="1"/>
+<wire x1="11.8937375" y1="33.4944" x2="16" y2="29.3881375" width="0.2032" layer="1"/>
+<wire x1="16" y1="29.3881375" x2="16" y2="17.8" width="0.2032" layer="1"/>
+<wire x1="16" y1="17.8" x2="13.1" y2="14.9" width="0.2032" layer="1"/>
+<wire x1="13.1" y1="14.9" x2="11.6" y2="14.9" width="0.2032" layer="1"/>
+<wire x1="11.6" y1="14.9" x2="10.73" y2="14.03" width="0.2032" layer="1"/>
+<wire x1="10.73" y1="14.03" x2="10.73" y2="13.2822" width="0.2032" layer="1"/>
+<wire x1="8.63966875" y1="36.6558375" x2="9.2" y2="36.09550625" width="0.2032" layer="16"/>
+<wire x1="9.2" y1="36.09550625" x2="9.2" y2="35.2" width="0.2032" layer="16"/>
+<wire x1="9.2" y1="35.2" x2="10.9056" y2="33.4944" width="0.2032" layer="16"/>
+<wire x1="10.9056" y1="33.4944" x2="11.8937375" y2="33.4944" width="0.2032" layer="16"/>
+</signal>
+<signal name="0-2-CS">
+<contactref element="U1" pad="13"/>
+<contactref element="U5" pad="1"/>
+<wire x1="7.1" y1="41.18" x2="7.1" y2="39.6029" width="0.2032" layer="1"/>
+<wire x1="7.1" y1="39.6029" x2="7.4556" y2="39.2473" width="0.2032" layer="1"/>
+<wire x1="7.4556" y1="39.2473" x2="7.4556" y2="36.93846875" width="0.2032" layer="1"/>
+<wire x1="11.36186875" y1="33.1322" x2="10.661875" y2="33.1322" width="0.2032" layer="16"/>
+<wire x1="10.661875" y1="33.1322" x2="8.6470375" y2="35.1470375" width="0.2032" layer="16"/>
+<via x="12.04703125" y="32.44703125" extent="1-16" drill="0.35"/>
+<wire x1="12.04703125" y1="32.44703125" x2="12.04703125" y2="32.4470375" width="0.2032" layer="16"/>
+<wire x1="12.04703125" y1="32.4470375" x2="11.36186875" y2="33.1322" width="0.2032" layer="16"/>
+<wire x1="12.04703125" y1="32.44703125" x2="15.4" y2="29.0940625" width="0.2032" layer="1"/>
+<wire x1="15.4" y1="29.0940625" x2="15.4" y2="18.2" width="0.2032" layer="1"/>
+<wire x1="15.4" y1="18.2" x2="12.8" y2="15.6" width="0.2032" layer="1"/>
+<wire x1="12.8" y1="15.6" x2="11.1" y2="15.6" width="0.2032" layer="1"/>
+<wire x1="11.1" y1="15.6" x2="10.095" y2="14.595" width="0.2032" layer="1"/>
+<wire x1="10.095" y1="14.595" x2="10.095" y2="13.2822" width="0.2032" layer="1"/>
+<via x="8.5678" y="35.7470375" extent="1-16" drill="0.35"/>
+<wire x1="7.4556" y1="36.93846875" x2="8.5678" y2="35.82626875" width="0.2032" layer="1"/>
+<wire x1="8.5678" y1="35.82626875" x2="8.5678" y2="35.7470375" width="0.2032" layer="1"/>
+<wire x1="8.6470375" y1="35.1470375" x2="8.6470375" y2="35.6678" width="0.2032" layer="16"/>
+<wire x1="8.6470375" y1="35.6678" x2="8.5678" y2="35.7470375" width="0.2032" layer="16"/>
+</signal>
+<signal name="0-3-MISO">
+<contactref element="U1" pad="14"/>
+<contactref element="U5" pad="3"/>
+<wire x1="6.3" y1="41.18" x2="6.3" y2="39.4" width="0.2032" layer="1"/>
+<wire x1="6.3" y1="39.4" x2="6.8" y2="38.9" width="0.2032" layer="1"/>
+<wire x1="6.8" y1="38.9" x2="6.8" y2="36.7" width="0.2032" layer="1"/>
+<wire x1="6.8" y1="36.7" x2="7.6" y2="35.9" width="0.2032" layer="1"/>
+<via x="7.6" y="35.9" extent="1-16" drill="0.35"/>
+<wire x1="11.1" y1="32.5" x2="10.7" y2="32.5" width="0.2032" layer="16"/>
+<wire x1="10.7" y1="32.5" x2="7.6" y2="35.6" width="0.2032" layer="16"/>
+<wire x1="7.6" y1="35.6" x2="7.6" y2="35.9" width="0.2032" layer="16"/>
+<via x="11.1" y="32.5" extent="1-16" drill="0.35"/>
+<wire x1="11.365" y1="13.2822" x2="11.365" y2="12.565" width="0.2032" layer="1"/>
+<wire x1="11.365" y1="12.565" x2="11" y2="12.2" width="0.2032" layer="1"/>
+<wire x1="11" y1="12.2" x2="9.7" y2="12.2" width="0.2032" layer="1"/>
+<wire x1="9.7" y1="12.2" x2="9.4" y2="12.5" width="0.2032" layer="1"/>
+<wire x1="9.4" y1="12.5" x2="9.4" y2="14.9" width="0.2032" layer="1"/>
+<wire x1="9.4" y1="14.9" x2="10.7" y2="16.2" width="0.2032" layer="1"/>
+<wire x1="10.7" y1="16.2" x2="12.4" y2="16.2" width="0.2032" layer="1"/>
+<wire x1="12.4" y1="16.2" x2="14.8" y2="18.6" width="0.2032" layer="1"/>
+<wire x1="14.8" y1="18.6" x2="14.8" y2="28.8" width="0.2032" layer="1"/>
+<wire x1="14.8" y1="28.8" x2="11.1" y2="32.5" width="0.2032" layer="1"/>
+</signal>
+</signals>
+<mfgpreviewcolors>
+<mfgpreviewcolor name="soldermaskcolor" color="0xC8008000"/>
+<mfgpreviewcolor name="silkscreencolor" color="0xFFFEFEFE"/>
+<mfgpreviewcolor name="backgroundcolor" color="0xFF282828"/>
+<mfgpreviewcolor name="coppercolor" color="0xFFFFBF00"/>
+<mfgpreviewcolor name="substratecolor" color="0xFF786E46"/>
+</mfgpreviewcolors>
+</board>
+</drawing>
+<compatibility>
+<note version="8.2" severity="warning">
+Since Version 8.2, EAGLE supports online libraries. The ids
+of those online libraries will not be understood (or retained)
+with this version.
+</note>
+<note version="8.3" severity="warning">
+Since Version 8.3, EAGLE supports URNs for individual library
+assets (packages, symbols, and devices). The URNs of those assets
+will not be understood (or retained) with this version.
+</note>
+<note version="8.3" severity="warning">
+Since Version 8.3, EAGLE supports the association of 3D packages
+with devices in libraries, schematics, and board files. Those 3D
+packages will not be understood (or retained) with this version.
+</note>
+</compatibility>
+</eagle>
diff --git a/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1_2023-10-31.zip b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1_2023-10-31.zip
new file mode 100644
index 0000000000000000000000000000000000000000..55edf336426df3ab4ad32c10bf1b633634fa35c1
Binary files /dev/null and b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v1_2023-10-31.zip differ
diff --git a/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v2.f3z b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v2.f3z
new file mode 100644
index 0000000000000000000000000000000000000000..6f60c5757550f2222ab919c6192b19065fcbccf8
Binary files /dev/null and b/system/ecad/lpf-filament-sensor/Filament Sensor Circuit v2.f3z differ
diff --git a/system/ecad/lpf-filament-sensor/Filament_Sensor_Circuit_v1.sch b/system/ecad/lpf-filament-sensor/Filament_Sensor_Circuit_v1.sch
new file mode 100644
index 0000000000000000000000000000000000000000..47d8206dbbc478e11634abdab11ad4388c960738
--- /dev/null
+++ b/system/ecad/lpf-filament-sensor/Filament_Sensor_Circuit_v1.sch
@@ -0,0 +1,3638 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE eagle SYSTEM "eagle.dtd">
+<eagle version="9.7.0">
+<drawing>
+<settings>
+<setting alwaysvectorfont="no"/>
+<setting verticaltext="up"/>
+</settings>
+<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>
+<layers>
+<layer number="1" name="Top" color="4" fill="1" visible="no" active="no"/>
+<layer number="2" name="Route2" color="16" fill="3" visible="no" active="no"/>
+<layer number="3" name="Route3" color="17" fill="3" visible="no" active="no"/>
+<layer number="4" name="Route4" color="18" fill="4" visible="no" active="no"/>
+<layer number="5" name="Route5" color="19" fill="4" visible="no" active="no"/>
+<layer number="6" name="Route6" color="25" fill="8" visible="no" active="no"/>
+<layer number="7" name="Route7" color="26" fill="8" visible="no" active="no"/>
+<layer number="8" name="Route8" color="27" fill="2" visible="no" active="no"/>
+<layer number="9" name="Route9" color="28" fill="2" visible="no" active="no"/>
+<layer number="10" name="Route10" color="29" fill="7" visible="no" active="no"/>
+<layer number="11" name="Route11" color="30" fill="7" visible="no" active="no"/>
+<layer number="12" name="Route12" color="20" fill="5" visible="no" active="no"/>
+<layer number="13" name="Route13" color="21" fill="5" visible="no" active="no"/>
+<layer number="14" name="Route14" color="22" fill="6" visible="no" active="no"/>
+<layer number="15" name="Route15" color="23" fill="6" visible="no" active="no"/>
+<layer number="16" name="Bottom" color="1" fill="1" visible="no" active="no"/>
+<layer number="17" name="Pads" color="2" fill="1" visible="no" active="no"/>
+<layer number="18" name="Vias" color="2" fill="1" visible="no" active="no"/>
+<layer number="19" name="Unrouted" color="6" fill="1" visible="no" active="no"/>
+<layer number="20" name="Dimension" color="24" fill="1" visible="no" active="no"/>
+<layer number="21" name="tPlace" color="7" fill="1" visible="no" active="no"/>
+<layer number="22" name="bPlace" color="7" fill="1" visible="no" active="no"/>
+<layer number="23" name="tOrigins" color="15" fill="1" visible="no" active="no"/>
+<layer number="24" name="bOrigins" color="15" fill="1" visible="no" active="no"/>
+<layer number="25" name="tNames" color="7" fill="1" visible="no" active="no"/>
+<layer number="26" name="bNames" color="7" fill="1" visible="no" active="no"/>
+<layer number="27" name="tValues" color="7" fill="1" visible="no" active="no"/>
+<layer number="28" name="bValues" color="7" fill="1" visible="no" active="no"/>
+<layer number="29" name="tStop" color="7" fill="3" visible="no" active="no"/>
+<layer number="30" name="bStop" color="7" fill="6" visible="no" active="no"/>
+<layer number="31" name="tCream" color="7" fill="4" visible="no" active="no"/>
+<layer number="32" name="bCream" color="7" fill="5" visible="no" active="no"/>
+<layer number="33" name="tFinish" color="6" fill="3" visible="no" active="no"/>
+<layer number="34" name="bFinish" color="6" fill="6" visible="no" active="no"/>
+<layer number="35" name="tGlue" color="7" fill="4" visible="no" active="no"/>
+<layer number="36" name="bGlue" color="7" fill="5" visible="no" active="no"/>
+<layer number="37" name="tTest" color="7" fill="1" visible="no" active="no"/>
+<layer number="38" name="bTest" color="7" fill="1" visible="no" active="no"/>
+<layer number="39" name="tKeepout" color="4" fill="11" visible="no" active="no"/>
+<layer number="40" name="bKeepout" color="1" fill="11" visible="no" active="no"/>
+<layer number="41" name="tRestrict" color="4" fill="10" visible="no" active="no"/>
+<layer number="42" name="bRestrict" color="1" fill="10" visible="no" active="no"/>
+<layer number="43" name="vRestrict" color="2" fill="10" visible="no" active="no"/>
+<layer number="44" name="Drills" color="7" fill="1" visible="no" active="no"/>
+<layer number="45" name="Holes" color="7" fill="1" visible="no" active="no"/>
+<layer number="46" name="Milling" color="3" fill="1" visible="no" active="no"/>
+<layer number="47" name="Measures" color="7" fill="1" visible="no" active="no"/>
+<layer number="48" name="Document" color="7" fill="1" visible="no" active="no"/>
+<layer number="49" name="Reference" color="7" fill="1" visible="no" active="no"/>
+<layer number="50" name="dxf" color="7" fill="1" visible="no" active="no"/>
+<layer number="51" name="tDocu" color="7" fill="1" visible="no" active="no"/>
+<layer number="52" name="bDocu" color="7" fill="1" visible="no" active="no"/>
+<layer number="53" name="tGND_GNDA" color="7" fill="9" visible="no" active="no"/>
+<layer number="54" name="bGND_GNDA" color="7" fill="9" visible="no" active="no"/>
+<layer number="56" name="wert" color="7" fill="1" visible="no" active="no"/>
+<layer number="57" name="tCAD" color="7" fill="1" visible="no" active="no"/>
+<layer number="59" name="tCarbon" color="7" fill="1" visible="no" active="no"/>
+<layer number="60" name="bCarbon" color="7" fill="1" visible="no" active="no"/>
+<layer number="88" name="SimResults" color="9" fill="1" visible="yes" active="yes"/>
+<layer number="89" name="SimProbes" color="9" fill="1" visible="yes" active="yes"/>
+<layer number="90" name="Modules" color="5" fill="1" visible="yes" active="yes"/>
+<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes"/>
+<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes"/>
+<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes"/>
+<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>
+<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes"/>
+<layer number="99" name="SpiceOrder" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="100" name="Muster" color="7" fill="1" visible="no" active="no"/>
+<layer number="101" name="Patch_Top" color="7" fill="4" visible="no" active="yes"/>
+<layer number="102" name="Vscore" color="7" fill="1" visible="no" active="yes"/>
+<layer number="103" name="tMap" color="7" fill="1" visible="no" active="yes"/>
+<layer number="104" name="Name" color="7" fill="1" visible="no" active="yes"/>
+<layer number="105" name="tPlate" color="7" fill="1" visible="no" active="yes"/>
+<layer number="106" name="bPlate" color="7" fill="1" visible="no" active="yes"/>
+<layer number="107" name="Crop" color="7" fill="1" visible="no" active="yes"/>
+<layer number="108" name="tplace-old" color="7" fill="1" visible="no" active="yes"/>
+<layer number="109" name="ref-old" color="7" fill="1" visible="no" active="yes"/>
+<layer number="110" name="fp0" color="7" fill="1" visible="no" active="yes"/>
+<layer number="111" name="LPC17xx" color="7" fill="1" visible="no" active="yes"/>
+<layer number="112" name="tSilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="113" name="IDFDebug" color="7" fill="1" visible="no" active="yes"/>
+<layer number="114" name="Badge_Outline" color="7" fill="1" visible="no" active="yes"/>
+<layer number="115" name="ReferenceISLANDS" color="7" fill="1" visible="no" active="yes"/>
+<layer number="116" name="Patch_BOT" color="7" fill="4" visible="no" active="yes"/>
+<layer number="117" name="BACKMAAT1" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="118" name="Rect_Pads" color="7" fill="1" visible="no" active="yes"/>
+<layer number="119" name="KAP_TEKEN" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="120" name="KAP_MAAT1" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="121" name="_tsilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="122" name="_bsilk" color="7" fill="1" visible="no" active="yes"/>
+<layer number="123" name="tTestmark" color="7" fill="1" visible="no" active="yes"/>
+<layer number="124" name="bTestmark" color="7" fill="1" visible="no" active="yes"/>
+<layer number="125" name="_tNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="126" name="_bNames" color="7" fill="1" visible="no" active="yes"/>
+<layer number="127" name="_tValues" color="7" fill="1" visible="no" active="yes"/>
+<layer number="128" name="_bValues" color="7" fill="1" visible="no" active="yes"/>
+<layer number="129" name="Mask" color="7" fill="1" visible="no" active="yes"/>
+<layer number="130" name="SMDSTROOK" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="131" name="tAdjust" color="7" fill="1" visible="no" active="yes"/>
+<layer number="132" name="bAdjust" color="7" fill="1" visible="no" active="yes"/>
+<layer number="133" name="bottom_silk" color="7" fill="1" visible="yes" active="yes"/>
+<layer number="144" name="Drill_legend" color="7" fill="1" visible="no" active="yes"/>
+<layer number="150" name="Notes" color="7" fill="1" visible="no" active="yes"/>
+<layer number="151" name="HeatSink" color="7" fill="1" visible="no" active="yes"/>
+<layer number="152" name="_bDocu" color="7" fill="1" visible="no" active="yes"/>
+<layer number="153" name="FabDoc1" color="7" fill="1" visible="no" active="yes"/>
+<layer number="154" name="FabDoc2" color="7" fill="1" visible="no" active="yes"/>
+<layer number="155" name="FabDoc3" color="7" fill="1" visible="no" active="yes"/>
+<layer number="199" name="Contour" color="7" fill="1" visible="no" active="yes"/>
+<layer number="200" name="200bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="201" name="201bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="202" name="202bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="203" name="203bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="204" name="204bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="205" name="205bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="206" name="206bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="207" name="207bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="208" name="208bmp" color="7" fill="10" visible="no" active="yes"/>
+<layer number="209" name="209bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="210" name="210bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="211" name="211bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="212" name="212bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="213" name="213bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="214" name="214bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="215" name="215bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="216" name="216bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="217" name="217bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="218" name="218bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="219" name="219bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="220" name="220bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="221" name="221bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="222" name="222bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="223" name="223bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="224" name="224bmp" color="7" fill="1" visible="no" active="no"/>
+<layer number="225" name="225bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="226" name="226bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="227" name="227bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="228" name="228bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="229" name="229bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="230" name="230bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="231" name="231bmp" color="7" fill="1" visible="no" active="yes"/>
+<layer number="232" name="Eagle3D_PG2" color="7" fill="1" visible="no" active="yes"/>
+<layer number="233" name="Eagle3D_PG3" color="7" fill="1" visible="no" active="yes"/>
+<layer number="248" name="Housing" color="7" fill="1" visible="no" active="yes"/>
+<layer number="249" name="Edge" color="7" fill="1" visible="no" active="yes"/>
+<layer number="250" name="Descript" color="7" fill="1" visible="no" active="no"/>
+<layer number="251" name="SMDround" color="7" fill="11" visible="no" active="no"/>
+<layer number="254" name="cooling" color="7" fill="1" visible="no" active="yes"/>
+<layer number="255" name="routoute" color="7" fill="1" visible="no" active="yes"/>
+</layers>
+<schematic xreflabel="%F%N/%S.%C%R" xrefpart="/%S.%C%R">
+<libraries>
+<library name="microcontrollers">
+<packages>
+<package name="TQFP-32">
+<wire x1="-3.55" y1="-3.55" x2="-3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="-3.55" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="-3.55" x2="-3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="3.55" x2="-3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="3.55" x2="-3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="3.25" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.25" x2="-3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.55" x2="-3.25" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.25" y1="-3.55" x2="3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="-3.55" x2="3.55" y2="-3.25" width="0.127" layer="21"/>
+<text x="-3.202909375" y="5.80526875" size="0.8135375" layer="25">&gt;NAME</text>
+<text x="-3.40625" y="-6.211390625" size="0.81429375" layer="27">&gt;VALUE</text>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="21"/>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="51"/>
+<smd name="1" x="-4.18" y="2.8" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="2" x="-4.18" y="2" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="3" x="-4.18" y="1.2" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="4" x="-4.18" y="0.4" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="5" x="-4.18" y="-0.4" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="6" x="-4.18" y="-1.2" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="7" x="-4.18" y="-2" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="8" x="-4.18" y="-2.8" dx="1.6" dy="0.55" layer="1" roundness="25"/>
+<smd name="9" x="-2.8" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="10" x="-2" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="11" x="-1.2" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="12" x="-0.4" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="13" x="0.4" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="14" x="1.2" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="15" x="2" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="16" x="2.8" y="-4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R90"/>
+<smd name="17" x="4.18" y="-2.8" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="18" x="4.18" y="-2" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="19" x="4.18" y="-1.2" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="20" x="4.18" y="-0.4" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="21" x="4.18" y="0.4" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="22" x="4.18" y="1.2" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="23" x="4.18" y="2" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="24" x="4.18" y="2.8" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R180"/>
+<smd name="25" x="2.8" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="26" x="2" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="27" x="1.2" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="28" x="0.4" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="29" x="-0.4" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="30" x="-1.2" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="31" x="-2" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+<smd name="32" x="-2.8" y="4.18" dx="1.6" dy="0.55" layer="1" roundness="25" rot="R270"/>
+</package>
+<package name="TQFP-32-FAB">
+<wire x1="-3.55" y1="-3.55" x2="-3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="-3.55" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="3.55" y1="-3.55" x2="-3.55" y2="-3.55" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="3.55" x2="-3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="3.55" x2="-3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="3.25" y1="3.55" x2="3.55" y2="3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="3.55" x2="3.55" y2="3.25" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.25" x2="-3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="-3.55" y1="-3.55" x2="-3.25" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.25" y1="-3.55" x2="3.55" y2="-3.55" width="0.127" layer="21"/>
+<wire x1="3.55" y1="-3.55" x2="3.55" y2="-3.25" width="0.127" layer="21"/>
+<text x="-3.202909375" y="5.80526875" size="0.8135375" layer="25">&gt;NAME</text>
+<text x="-3.40625" y="-6.211390625" size="0.81429375" layer="27">&gt;VALUE</text>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="21"/>
+<circle x="-5.8" y="2.8" radius="0.1" width="0.2" layer="51"/>
+<smd name="1" x="-4.355" y="2.8" dx="1.25" dy="0.35" layer="1" roundness="25"/>
+<smd name="2" x="-4.18" y="2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="3" x="-4.18" y="1.2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="4" x="-4.18" y="0.4" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="5" x="-4.18" y="-0.4" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="6" x="-4.18" y="-1.2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="7" x="-4.18" y="-2" dx="1.6" dy="0.35" layer="1" roundness="25"/>
+<smd name="8" x="-4.355" y="-2.8" dx="1.25" dy="0.35" layer="1" roundness="25"/>
+<smd name="9" x="-2.8" y="-4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="10" x="-2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="11" x="-1.2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="12" x="-0.4" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="13" x="0.4" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="14" x="1.2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="15" x="2" y="-4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="16" x="2.8" y="-4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R90"/>
+<smd name="17" x="4.355" y="-2.8" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="18" x="4.18" y="-2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="19" x="4.18" y="-1.2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="20" x="4.18" y="-0.4" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="21" x="4.18" y="0.4" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="22" x="4.18" y="1.2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="23" x="4.18" y="2" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="24" x="4.355" y="2.8" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R180"/>
+<smd name="25" x="2.8" y="4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="26" x="2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="27" x="1.2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="28" x="0.4" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="29" x="-0.4" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="30" x="-1.2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="31" x="-2" y="4.18" dx="1.6" dy="0.35" layer="1" roundness="25" rot="R270"/>
+<smd name="32" x="-2.8" y="4.355" dx="1.25" dy="0.35" layer="1" roundness="25" rot="R270"/>
+</package>
+</packages>
+<symbols>
+<symbol name="ATSAMD21E18A-AF">
+<wire x1="48.26" y1="-33.02" x2="-20.32" y2="-33.02" width="0.254" layer="94"/>
+<wire x1="-20.32" y1="-33.02" x2="-20.32" y2="35.56" width="0.254" layer="94"/>
+<wire x1="-20.32" y1="35.56" x2="48.26" y2="35.56" width="0.254" layer="94"/>
+<wire x1="48.26" y1="35.56" x2="48.26" y2="-33.02" width="0.254" layer="94"/>
+<text x="-20.3338" y="35.5978" size="1.780409375" layer="95">&gt;NAME</text>
+<text x="-20.338" y="-35.614" size="1.78115" layer="96">&gt;VALUE</text>
+<pin name="(ADA_PXL)PA00/TCC2-0/SER1-0/XIN32" x="53.34" y="33.02" length="middle" rot="R180"/>
+<pin name="(ADA_PXL)PA01/TCC2-1/SER1-1/XOUT32" x="53.34" y="30.48" length="middle" rot="R180"/>
+<pin name="(ADA_D1A1)PA02/AIN-0/DAC-0" x="53.34" y="27.94" length="middle" rot="R180"/>
+<pin name="PA03/VREFA" x="53.34" y="25.4" length="middle" rot="R180"/>
+<pin name="(ADA_D0_TX)PA04/VREFB/AIN4/AIN0/TCC0-0/SER0-0" x="53.34" y="22.86" length="middle" rot="R180"/>
+<pin name="(ADA_D2_RX)PA05/AIN5/AIN1/TCC0-1/SER0-1" x="53.34" y="20.32" length="middle" rot="R180"/>
+<pin name="PA06/AIN6/AIN2/TCC1-0/SER0-2" x="53.34" y="17.78" length="middle" rot="R180"/>
+<pin name="PA07/AIN7/AIN3/TCC1-1/SER0-3" x="53.34" y="15.24" length="middle" rot="R180"/>
+<pin name="VDDANA" x="-25.4" y="25.4" length="middle" direction="pwr"/>
+<pin name="GND" x="-25.4" y="-30.48" length="middle" direction="pwr"/>
+<pin name="PA08/AIN16/TCC0-0/TCC1-2/SER0-0/SER2-0" x="53.34" y="12.7" length="middle" rot="R180"/>
+<pin name="PA09/AIN17/TCC0-1/TCC1-3/SER0-1/SER2-1" x="53.34" y="10.16" length="middle" rot="R180"/>
+<pin name="PA10/AIN18/TCC0-2/TCC1-0/SER0-2/SER2-2" x="53.34" y="7.62" length="middle" rot="R180"/>
+<pin name="PA11/AIN19/TCC0-3/TCC1-1/SER0-3/SER2-3" x="53.34" y="5.08" length="middle" rot="R180"/>
+<pin name="PA14/TC3-1/TCC0-4/SER2-2/XIN" x="53.34" y="2.54" length="middle" rot="R180"/>
+<pin name="PA15/TC3-1/TCC0-5/SER2-3/XOUT" x="53.34" y="0" length="middle" rot="R180"/>
+<pin name="PA16/TCC2-0/TCC0-6/SER1-0/SER3-0" x="53.34" y="-2.54" length="middle" rot="R180"/>
+<pin name="PA17/TCC2-1/TCC0-7/SER1-1/SER3-1" x="53.34" y="-5.08" length="middle" rot="R180"/>
+<pin name="PA18/TC3-0/TCC0-2/SER1-2/SER3-2" x="53.34" y="-7.62" length="middle" rot="R180"/>
+<pin name="PA19/TC3-1/TCC0-3/SER1-3/SER3-3" x="53.34" y="-10.16" length="middle" rot="R180"/>
+<pin name="PA22/TC4-0/TCC0-4/SER3-0" x="53.34" y="-12.7" length="middle" rot="R180"/>
+<pin name="(ADA_D13)PA23/TC4-1/TCC0-5/SER3-1/USB-SOF" x="53.34" y="-15.24" length="middle" rot="R180"/>
+<pin name="PA24/TC5-0/TCC1-2/SER3-2/USB-DM" x="53.34" y="-17.78" length="middle" rot="R180"/>
+<pin name="PA25/TC5-1/TCC1-3/SER3-3/USB-DP" x="53.34" y="-20.32" length="middle" rot="R180"/>
+<pin name="PA27" x="53.34" y="-22.86" length="middle" rot="R180"/>
+<pin name="!RESET" x="-25.4" y="-17.78" length="middle" direction="in"/>
+<pin name="PA28" x="53.34" y="-25.4" length="middle" rot="R180"/>
+<pin name="VDDCORE" x="-25.4" y="17.78" length="middle" direction="pwr"/>
+<pin name="VDDIN" x="-25.4" y="33.02" length="middle" direction="pwr"/>
+<pin name="PA30/TCC1-0/SER1-2/SWDCLK" x="53.34" y="-27.94" length="middle" rot="R180"/>
+<pin name="PA31/TCC1-1/SER1-3/SWDIO" x="53.34" y="-30.48" length="middle" rot="R180"/>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="ATSAMD21E18A-AF" prefix="U">
+<description>The SAM D21 is a series of low-power microcontrollers using the 32-bit ARM®
+ Cortex®
+-M0+ processor,
+and ranging from 32- to 64-pins with up to 256KB Flash and 32KB of SRAM. The SAM D21 operate at a
+maximum frequency of 48MHz and reach 2.46 CoreMark®
+/MHz. &lt;a href="https://pricing.snapeda.com/parts/ATSAMD21E18A-AF/Microchip/view-part?ref=eda"&gt;Check prices&lt;/a&gt;</description>
+<gates>
+<gate name="G$1" symbol="ATSAMD21E18A-AF" x="0" y="0"/>
+</gates>
+<devices>
+<device name="" package="TQFP-32">
+<connects>
+<connect gate="G$1" pin="!RESET" pad="26"/>
+<connect gate="G$1" pin="(ADA_D0_TX)PA04/VREFB/AIN4/AIN0/TCC0-0/SER0-0" pad="5"/>
+<connect gate="G$1" pin="(ADA_D13)PA23/TC4-1/TCC0-5/SER3-1/USB-SOF" pad="22"/>
+<connect gate="G$1" pin="(ADA_D1A1)PA02/AIN-0/DAC-0" pad="3"/>
+<connect gate="G$1" pin="(ADA_D2_RX)PA05/AIN5/AIN1/TCC0-1/SER0-1" pad="6"/>
+<connect gate="G$1" pin="(ADA_PXL)PA00/TCC2-0/SER1-0/XIN32" pad="1"/>
+<connect gate="G$1" pin="(ADA_PXL)PA01/TCC2-1/SER1-1/XOUT32" pad="2"/>
+<connect gate="G$1" pin="GND" pad="10 28"/>
+<connect gate="G$1" pin="PA03/VREFA" pad="4"/>
+<connect gate="G$1" pin="PA06/AIN6/AIN2/TCC1-0/SER0-2" pad="7"/>
+<connect gate="G$1" pin="PA07/AIN7/AIN3/TCC1-1/SER0-3" pad="8"/>
+<connect gate="G$1" pin="PA08/AIN16/TCC0-0/TCC1-2/SER0-0/SER2-0" pad="11"/>
+<connect gate="G$1" pin="PA09/AIN17/TCC0-1/TCC1-3/SER0-1/SER2-1" pad="12"/>
+<connect gate="G$1" pin="PA10/AIN18/TCC0-2/TCC1-0/SER0-2/SER2-2" pad="13"/>
+<connect gate="G$1" pin="PA11/AIN19/TCC0-3/TCC1-1/SER0-3/SER2-3" pad="14"/>
+<connect gate="G$1" pin="PA14/TC3-1/TCC0-4/SER2-2/XIN" pad="15"/>
+<connect gate="G$1" pin="PA15/TC3-1/TCC0-5/SER2-3/XOUT" pad="16"/>
+<connect gate="G$1" pin="PA16/TCC2-0/TCC0-6/SER1-0/SER3-0" pad="17"/>
+<connect gate="G$1" pin="PA17/TCC2-1/TCC0-7/SER1-1/SER3-1" pad="18"/>
+<connect gate="G$1" pin="PA18/TC3-0/TCC0-2/SER1-2/SER3-2" pad="19"/>
+<connect gate="G$1" pin="PA19/TC3-1/TCC0-3/SER1-3/SER3-3" pad="20"/>
+<connect gate="G$1" pin="PA22/TC4-0/TCC0-4/SER3-0" pad="21"/>
+<connect gate="G$1" pin="PA24/TC5-0/TCC1-2/SER3-2/USB-DM" pad="23"/>
+<connect gate="G$1" pin="PA25/TC5-1/TCC1-3/SER3-3/USB-DP" pad="24"/>
+<connect gate="G$1" pin="PA27" pad="25"/>
+<connect gate="G$1" pin="PA28" pad="27"/>
+<connect gate="G$1" pin="PA30/TCC1-0/SER1-2/SWDCLK" pad="31"/>
+<connect gate="G$1" pin="PA31/TCC1-1/SER1-3/SWDIO" pad="32"/>
+<connect gate="G$1" pin="VDDANA" pad="9"/>
+<connect gate="G$1" pin="VDDCORE" pad="29"/>
+<connect gate="G$1" pin="VDDIN" pad="30"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="AVAILABILITY" value="Warning"/>
+<attribute name="DESCRIPTION" value=" ARM® Cortex®-M0+ Automotive, AEC-Q100, SAM D21E, Functional Safety (FuSa) Microcontroller IC 32-Bit 48MHz 256KB (256K x 8) FLASH 32-TQFP (7x7) "/>
+<attribute name="MF" value="Microchip"/>
+<attribute name="MP" value="ATSAMD21E18A-AF"/>
+<attribute name="PACKAGE" value="TQFP-32 Microchip"/>
+<attribute name="PRICE" value="None"/>
+<attribute name="PURCHASE-URL" value="https://pricing.snapeda.com/search/part/ATSAMD21E18A-AF/?ref=eda"/>
+</technology>
+</technologies>
+</device>
+<device name="FAB" package="TQFP-32-FAB">
+<connects>
+<connect gate="G$1" pin="!RESET" pad="26"/>
+<connect gate="G$1" pin="(ADA_D0_TX)PA04/VREFB/AIN4/AIN0/TCC0-0/SER0-0" pad="5"/>
+<connect gate="G$1" pin="(ADA_D13)PA23/TC4-1/TCC0-5/SER3-1/USB-SOF" pad="22"/>
+<connect gate="G$1" pin="(ADA_D1A1)PA02/AIN-0/DAC-0" pad="3"/>
+<connect gate="G$1" pin="(ADA_D2_RX)PA05/AIN5/AIN1/TCC0-1/SER0-1" pad="6"/>
+<connect gate="G$1" pin="(ADA_PXL)PA00/TCC2-0/SER1-0/XIN32" pad="1"/>
+<connect gate="G$1" pin="(ADA_PXL)PA01/TCC2-1/SER1-1/XOUT32" pad="2"/>
+<connect gate="G$1" pin="GND" pad="10 28"/>
+<connect gate="G$1" pin="PA03/VREFA" pad="4"/>
+<connect gate="G$1" pin="PA06/AIN6/AIN2/TCC1-0/SER0-2" pad="7"/>
+<connect gate="G$1" pin="PA07/AIN7/AIN3/TCC1-1/SER0-3" pad="8"/>
+<connect gate="G$1" pin="PA08/AIN16/TCC0-0/TCC1-2/SER0-0/SER2-0" pad="11"/>
+<connect gate="G$1" pin="PA09/AIN17/TCC0-1/TCC1-3/SER0-1/SER2-1" pad="12"/>
+<connect gate="G$1" pin="PA10/AIN18/TCC0-2/TCC1-0/SER0-2/SER2-2" pad="13"/>
+<connect gate="G$1" pin="PA11/AIN19/TCC0-3/TCC1-1/SER0-3/SER2-3" pad="14"/>
+<connect gate="G$1" pin="PA14/TC3-1/TCC0-4/SER2-2/XIN" pad="15"/>
+<connect gate="G$1" pin="PA15/TC3-1/TCC0-5/SER2-3/XOUT" pad="16"/>
+<connect gate="G$1" pin="PA16/TCC2-0/TCC0-6/SER1-0/SER3-0" pad="17"/>
+<connect gate="G$1" pin="PA17/TCC2-1/TCC0-7/SER1-1/SER3-1" pad="18"/>
+<connect gate="G$1" pin="PA18/TC3-0/TCC0-2/SER1-2/SER3-2" pad="19"/>
+<connect gate="G$1" pin="PA19/TC3-1/TCC0-3/SER1-3/SER3-3" pad="20"/>
+<connect gate="G$1" pin="PA22/TC4-0/TCC0-4/SER3-0" pad="21"/>
+<connect gate="G$1" pin="PA24/TC5-0/TCC1-2/SER3-2/USB-DM" pad="23"/>
+<connect gate="G$1" pin="PA25/TC5-1/TCC1-3/SER3-3/USB-DP" pad="24"/>
+<connect gate="G$1" pin="PA27" pad="25"/>
+<connect gate="G$1" pin="PA28" pad="27"/>
+<connect gate="G$1" pin="PA30/TCC1-0/SER1-2/SWDCLK" pad="31"/>
+<connect gate="G$1" pin="PA31/TCC1-1/SER1-3/SWDIO" pad="32"/>
+<connect gate="G$1" pin="VDDANA" pad="9"/>
+<connect gate="G$1" pin="VDDCORE" pad="29"/>
+<connect gate="G$1" pin="VDDIN" pad="30"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="SparkFun-Connectors" urn="urn:adsk.eagle:library:513">
+<description>&lt;h3&gt;SparkFun Connectors&lt;/h3&gt;
+This library contains electrically-functional connectors. 
+&lt;br&gt;
+&lt;br&gt;
+We've spent an enormous amount of time creating and checking these footprints and parts, but it is &lt;b&gt; the end user's responsibility&lt;/b&gt; to ensure correctness and suitablity for a given componet or application. 
+&lt;br&gt;
+&lt;br&gt;If you enjoy using this library, please buy one of our products at &lt;a href=" www.sparkfun.com"&gt;SparkFun.com&lt;/a&gt;.
+&lt;br&gt;
+&lt;br&gt;
+&lt;b&gt;Licensing:&lt;/b&gt; Creative Commons ShareAlike 4.0 International - https://creativecommons.org/licenses/by-sa/4.0/ 
+&lt;br&gt;
+&lt;br&gt;
+You are welcome to use this library for commercial purposes. For attribution, we ask that when you begin to sell your device using our footprint, you email us with a link to the product being sold. We want bragging rights that we helped (in a very small part) to create your 8th world wonder. We would like the opportunity to feature your device on our homepage.</description>
+<packages>
+<package name="SAMTECH_FTSH-105-01" urn="urn:adsk.eagle:footprint:37965/1" library_version="1">
+<description>&lt;h3&gt;ARM Cortex Debug Connector (10-pin)&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.05"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href="https://www.samtec.com/ftppub/cpdf/FTSH-1XX-XX-XXX-DV-XXX-MKT.pdf"&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CORTEX_DEBUG&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="3.175" y1="1.7145" x2="3.175" y2="-1.7145" width="0.127" layer="51"/>
+<wire x1="3.175" y1="-1.7145" x2="-3.175" y2="-1.7145" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="-1.7145" x2="-3.175" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="-3.175" y1="1.7145" x2="3.175" y2="1.7145" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="-1.7145" x2="-3.175" y2="1.7145" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="1.7145" x2="-3.0226" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="3.0226" y1="1.7145" x2="3.175" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="3.175" y1="1.7145" x2="3.175" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="3.175" y1="-1.7145" x2="3.0226" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="-3.0226" y1="-1.7145" x2="-3.175" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="-2.0574" y1="1.7145" x2="-1.7526" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="-0.7874" y1="1.7145" x2="-0.4826" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="0.4826" y1="1.7145" x2="0.7874" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="1.7526" y1="1.7145" x2="2.0574" y2="1.7145" width="0.2032" layer="21"/>
+<wire x1="2.0574" y1="-1.7145" x2="1.7526" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="0.7874" y1="-1.7145" x2="0.4826" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="-0.4826" y1="-1.7145" x2="-0.7874" y2="-1.7145" width="0.2032" layer="21"/>
+<wire x1="-1.7526" y1="-1.7145" x2="-2.0574" y2="-1.7145" width="0.2032" layer="21"/>
+<rectangle x1="-0.2032" y1="1.7145" x2="0.2032" y2="2.921" layer="51"/>
+<rectangle x1="1.0668" y1="1.7145" x2="1.4732" y2="2.921" layer="51"/>
+<rectangle x1="2.3368" y1="1.7145" x2="2.7432" y2="2.921" layer="51"/>
+<rectangle x1="-1.4732" y1="1.7145" x2="-1.0668" y2="2.921" layer="51"/>
+<rectangle x1="-2.7432" y1="1.7145" x2="-2.3368" y2="2.921" layer="51"/>
+<rectangle x1="-0.2032" y1="-2.921" x2="0.2032" y2="-1.7145" layer="51" rot="R180"/>
+<rectangle x1="-1.4732" y1="-2.921" x2="-1.0668" y2="-1.7145" layer="51" rot="R180"/>
+<rectangle x1="-2.7432" y1="-2.921" x2="-2.3368" y2="-1.7145" layer="51" rot="R180"/>
+<rectangle x1="1.0668" y1="-2.921" x2="1.4732" y2="-1.7145" layer="51" rot="R180"/>
+<rectangle x1="2.3368" y1="-2.921" x2="2.7432" y2="-1.7145" layer="51" rot="R180"/>
+<smd name="6" x="0" y="2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="8" x="1.27" y="2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="10" x="2.54" y="2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="4" x="-1.27" y="2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="2" x="-2.54" y="2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="1" x="-2.54" y="-2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="3" x="-1.27" y="-2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="5" x="0" y="-2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="7" x="1.27" y="-2.413" dx="0.508" dy="1.27" layer="1"/>
+<smd name="9" x="2.54" y="-2.413" dx="0.508" dy="1.27" layer="1"/>
+<text x="-1.3462" y="0.4572" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-1.7018" y="-0.9652" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+<package name="2X5-PTH-1.27MM" urn="urn:adsk.eagle:footprint:37966/1" library_version="1">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 ARM Cortex Debug Connector (10-pin)&lt;/h3&gt;
+&lt;p&gt;tDoc (51) layer border represents maximum dimensions of plastic housing.&lt;/p&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:1.27mm&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”http://portal.fciconnect.com/Comergent//fci/drawing/20021111.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<pad name="8" x="1.27" y="0.635" drill="0.508" diameter="1"/>
+<pad name="6" x="0" y="0.635" drill="0.508" diameter="1"/>
+<pad name="4" x="-1.27" y="0.635" drill="0.508" diameter="1"/>
+<pad name="2" x="-2.54" y="0.635" drill="0.508" diameter="1"/>
+<pad name="10" x="2.54" y="0.635" drill="0.508" diameter="1"/>
+<pad name="7" x="1.27" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="5" x="0" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="3" x="-1.27" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="1" x="-2.54" y="-0.635" drill="0.508" diameter="1"/>
+<pad name="9" x="2.54" y="-0.635" drill="0.508" diameter="1"/>
+<wire x1="-3.403" y1="-1.021" x2="-3.403" y2="-0.259" width="0.254" layer="21"/>
+<wire x1="3.175" y1="1.715" x2="-3.175" y2="1.715" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="1.715" x2="-3.175" y2="-1.715" width="0.127" layer="51"/>
+<wire x1="-3.175" y1="-1.715" x2="3.175" y2="-1.715" width="0.127" layer="51"/>
+<wire x1="3.175" y1="-1.715" x2="3.175" y2="1.715" width="0.127" layer="51"/>
+<text x="-1.5748" y="1.9304" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-1.8288" y="-2.4638" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+</packages>
+<packages3d>
+<package3d name="SAMTECH_FTSH-105-01" urn="urn:adsk.eagle:package:38289/1" type="box" library_version="1">
+<description>ARM Cortex Debug Connector (10-pin)
+Specifications:
+Pin count:10
+Pin pitch:0.05"
+
+Datasheet referenced for footprint
+Example device(s):
+CORTEX_DEBUG
+</description>
+<packageinstances>
+<packageinstance name="SAMTECH_FTSH-105-01"/>
+</packageinstances>
+</package3d>
+<package3d name="2X5-PTH-1.27MM" urn="urn:adsk.eagle:package:38290/1" type="box" library_version="1">
+<description>Plated Through Hole - 2x5 ARM Cortex Debug Connector (10-pin)
+tDoc (51) layer border represents maximum dimensions of plastic housing.
+Specifications:
+Pin count:10
+Pin pitch:1.27mm
+
+Datasheet referenced for footprint
+Example device(s):
+CONN_05x2
+</description>
+<packageinstances>
+<packageinstance name="2X5-PTH-1.27MM"/>
+</packageinstances>
+</package3d>
+</packages3d>
+<symbols>
+<symbol name="CORTEX_DEBUG" urn="urn:adsk.eagle:symbol:37964/1" library_version="1">
+<description>&lt;h3&gt;Cortex Debug Connector&lt;/h3&gt;
+&lt;p&gt;&lt;a href="http://infocenter.arm.com/help/topic/com.arm.doc.faqs/attached/13634/cortex_debug_connectors.pdf"&gt;Datasheet&lt;/a&gt;&lt;/p&gt;</description>
+<pin name="VCC" x="-15.24" y="5.08" length="short"/>
+<pin name="GND@3" x="-15.24" y="2.54" length="short"/>
+<pin name="GND@5" x="-15.24" y="0" length="short"/>
+<pin name="KEY" x="-15.24" y="-2.54" length="short"/>
+<pin name="GNDDTCT" x="-15.24" y="-5.08" length="short"/>
+<pin name="!RESET" x="17.78" y="-5.08" length="short" rot="R180"/>
+<pin name="NC/TDI" x="17.78" y="-2.54" length="short" rot="R180"/>
+<pin name="SWO/TDO" x="17.78" y="0" length="short" rot="R180"/>
+<pin name="SWDCLK/TCK" x="17.78" y="2.54" length="short" rot="R180"/>
+<pin name="SWDIO/TMS" x="17.78" y="5.08" length="short" rot="R180"/>
+<wire x1="-12.7" y1="-7.62" x2="-12.7" y2="7.62" width="0.254" layer="94"/>
+<wire x1="-12.7" y1="7.62" x2="15.24" y2="7.62" width="0.254" layer="94"/>
+<wire x1="15.24" y1="7.62" x2="15.24" y2="-7.62" width="0.254" layer="94"/>
+<wire x1="15.24" y1="-7.62" x2="-12.7" y2="-7.62" width="0.254" layer="94"/>
+<text x="-12.7" y="7.874" size="1.778" layer="95" font="vector">&gt;Name</text>
+<text x="-12.7" y="-9.906" size="1.778" layer="96" font="vector">&gt;Value</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="CORTEX_DEBUG" urn="urn:adsk.eagle:component:38384/1" prefix="J" library_version="1">
+<description>&lt;h3&gt;Cortex Debug Connector - 10 pin&lt;/h3&gt;
+&lt;p&gt;Supports JTAG debug, Serial Wire debug, and Serial Wire Viewer.
+PTH and SMD connector options available.&lt;/p&gt;
+&lt;p&gt; &lt;ul&gt;&lt;a href=”http://infocenter.arm.com/help/topic/com.arm.doc.faqs/attached/13634/cortex_debug_connectors.pdf”&gt;General Connector Information&lt;/a&gt;
+&lt;p&gt;&lt;b&gt; Products:&lt;/b&gt;
+&lt;ul&gt;&lt;li&gt;&lt;a href=”http://www.digikey.com/product-detail/en/cnc-tech/3220-10-0100-00/1175-1627-ND/3883661”&gt;PTH Connector&lt;/a&gt; -via Digi-Key&lt;/li&gt;
+&lt;li&gt;&lt;a href=”https://www.sparkfun.com/products/13229”&gt;SparkFun PSoc&lt;/a&gt;&lt;/li&gt;
+&lt;li&gt;&lt;a href=”https://www.sparkfun.com/products/13810”&gt;SparkFun T&lt;/a&gt;&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<gates>
+<gate name="G$1" symbol="CORTEX_DEBUG" x="0" y="0"/>
+</gates>
+<devices>
+<device name="_SMD" package="SAMTECH_FTSH-105-01">
+<connects>
+<connect gate="G$1" pin="!RESET" pad="10"/>
+<connect gate="G$1" pin="GND@3" pad="3"/>
+<connect gate="G$1" pin="GND@5" pad="5"/>
+<connect gate="G$1" pin="GNDDTCT" pad="9"/>
+<connect gate="G$1" pin="KEY" pad="7"/>
+<connect gate="G$1" pin="NC/TDI" pad="8"/>
+<connect gate="G$1" pin="SWDCLK/TCK" pad="4"/>
+<connect gate="G$1" pin="SWDIO/TMS" pad="2"/>
+<connect gate="G$1" pin="SWO/TDO" pad="6"/>
+<connect gate="G$1" pin="VCC" pad="1"/>
+</connects>
+<package3dinstances>
+<package3dinstance package3d_urn="urn:adsk.eagle:package:38289/1"/>
+</package3dinstances>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="_PTH" package="2X5-PTH-1.27MM">
+<connects>
+<connect gate="G$1" pin="!RESET" pad="10"/>
+<connect gate="G$1" pin="GND@3" pad="3"/>
+<connect gate="G$1" pin="GND@5" pad="5"/>
+<connect gate="G$1" pin="GNDDTCT" pad="9"/>
+<connect gate="G$1" pin="KEY" pad="7"/>
+<connect gate="G$1" pin="NC/TDI" pad="8"/>
+<connect gate="G$1" pin="SWDCLK/TCK" pad="4"/>
+<connect gate="G$1" pin="SWDIO/TMS" pad="2"/>
+<connect gate="G$1" pin="SWO/TDO" pad="6"/>
+<connect gate="G$1" pin="VCC" pad="1"/>
+</connects>
+<package3dinstances>
+<package3dinstance package3d_urn="urn:adsk.eagle:package:38290/1"/>
+</package3dinstances>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="supply1" urn="urn:adsk.eagle:library:371">
+<description>&lt;b&gt;Supply Symbols&lt;/b&gt;&lt;p&gt;
+ GND, VCC, 0V, +5V, -5V, etc.&lt;p&gt;
+ Please keep in mind, that these devices are necessary for the
+ automatic wiring of the supply signals.&lt;p&gt;
+ The pin name defined in the symbol is identical to the net which is to be wired automatically.&lt;p&gt;
+ In this library the device names are the same as the pin names of the symbols, therefore the correct signal names appear next to the supply symbols in the schematic.&lt;p&gt;
+ &lt;author&gt;Created by librarian@cadsoft.de&lt;/author&gt;</description>
+<packages>
+</packages>
+<symbols>
+<symbol name="+5V" urn="urn:adsk.eagle:symbol:26929/1" library_version="1">
+<wire x1="1.27" y1="-1.905" x2="0" y2="0" width="0.254" layer="94"/>
+<wire x1="0" y1="0" x2="-1.27" y2="-1.905" width="0.254" layer="94"/>
+<text x="-2.54" y="-5.08" size="1.778" layer="96" rot="R90">&gt;VALUE</text>
+<pin name="+5V" x="0" y="-2.54" visible="off" length="short" direction="sup" rot="R90"/>
+</symbol>
+<symbol name="+3V3" urn="urn:adsk.eagle:symbol:26950/1" library_version="1">
+<wire x1="1.27" y1="-1.905" x2="0" y2="0" width="0.254" layer="94"/>
+<wire x1="0" y1="0" x2="-1.27" y2="-1.905" width="0.254" layer="94"/>
+<text x="-2.54" y="-5.08" size="1.778" layer="96" rot="R90">&gt;VALUE</text>
+<pin name="+3V3" x="0" y="-2.54" visible="off" length="short" direction="sup" rot="R90"/>
+</symbol>
+<symbol name="GND" urn="urn:adsk.eagle:symbol:26925/1" library_version="1">
+<wire x1="-1.905" y1="0" x2="1.905" y2="0" width="0.254" layer="94"/>
+<text x="-2.54" y="-2.54" size="1.778" layer="96">&gt;VALUE</text>
+<pin name="GND" x="0" y="2.54" visible="off" length="short" direction="sup" rot="R270"/>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="+5V" urn="urn:adsk.eagle:component:26963/1" prefix="P+" library_version="1">
+<description>&lt;b&gt;SUPPLY SYMBOL&lt;/b&gt;</description>
+<gates>
+<gate name="1" symbol="+5V" x="0" y="0"/>
+</gates>
+<devices>
+<device name="">
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+<deviceset name="+3V3" urn="urn:adsk.eagle:component:26981/1" prefix="+3V3" library_version="1">
+<description>&lt;b&gt;SUPPLY SYMBOL&lt;/b&gt;</description>
+<gates>
+<gate name="G$1" symbol="+3V3" x="0" y="0"/>
+</gates>
+<devices>
+<device name="">
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+<deviceset name="GND" urn="urn:adsk.eagle:component:26954/1" prefix="GND" library_version="1">
+<description>&lt;b&gt;SUPPLY SYMBOL&lt;/b&gt;</description>
+<gates>
+<gate name="1" symbol="GND" x="0" y="0"/>
+</gates>
+<devices>
+<device name="">
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="passives">
+<packages>
+<package name="TACT-SWITCH-KMR6">
+<smd name="P$1" x="-2.05" y="0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$2" x="2.05" y="0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$3" x="-2.05" y="-0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<smd name="P$4" x="2.05" y="-0.8" dx="0.9" dy="1" layer="1" rot="R180"/>
+<wire x1="-1.4" y1="0.8" x2="0" y2="0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="1.4" y2="0.8" width="0.127" layer="51"/>
+<wire x1="-1.4" y1="-0.8" x2="0" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="1.4" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="0" y2="0.6" width="0.127" layer="51"/>
+<wire x1="0" y1="0.6" x2="0.4" y2="-0.4" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="0" y2="-0.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="0.2" x2="-2.1" y2="-0.2" width="0.127" layer="51"/>
+<wire x1="2.1" y1="-0.2" x2="2.1" y2="0.2" width="0.127" layer="51"/>
+<wire x1="2.1" y1="1.4" x2="2.1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="2.1" y1="1.5" x2="1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="1.032" y1="1.5" x2="-2.1" y2="1.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="1.5" x2="-2.1" y2="1.4" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="-1.4" x2="-2.1" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="-2.1" y1="-1.5" x2="2.1" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="2.1" y1="-1.5" x2="2.1" y2="-1.4" width="0.127" layer="51"/>
+</package>
+<package name="TACT-SWITCH-SIDE">
+<smd name="P$1" x="-1.8" y="0.725" dx="1.4" dy="1.05" layer="1" rot="R180"/>
+<smd name="P$2" x="1.8" y="0.725" dx="1.4" dy="1.05" layer="1" rot="R180"/>
+<smd name="P$3" x="-1.8" y="-0.725" dx="1.4" dy="1.05" layer="1" rot="R180"/>
+<smd name="P$4" x="1.8" y="-0.725" dx="1.4" dy="1.05" layer="1" rot="R180"/>
+<wire x1="-0.9" y1="0.8" x2="0" y2="0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="0.9" y2="0.8" width="0.127" layer="51"/>
+<wire x1="-0.9" y1="-0.8" x2="0" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="0.9" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="0" y1="0.8" x2="0" y2="0.6" width="0.127" layer="51"/>
+<wire x1="0" y1="0.6" x2="0.4" y2="-0.4" width="0.127" layer="51"/>
+<wire x1="0" y1="-0.8" x2="0" y2="-0.5" width="0.127" layer="51"/>
+<wire x1="-1.75" y1="-1.45" x2="1.75" y2="-1.45" width="0.127" layer="21"/>
+<wire x1="-1.75" y1="1.6" x2="-1" y2="1.6" width="0.127" layer="21"/>
+<wire x1="-1" y1="1.6" x2="0" y2="1.6" width="0.127" layer="21"/>
+<wire x1="0" y1="1.6" x2="1" y2="1.6" width="0.127" layer="21"/>
+<wire x1="1" y1="1.6" x2="1.75" y2="1.6" width="0.127" layer="21"/>
+<wire x1="-1" y1="1.6" x2="-1" y2="2.3" width="0.127" layer="21"/>
+<wire x1="-1" y1="2.3" x2="1" y2="2.3" width="0.127" layer="21"/>
+<wire x1="1" y1="2.3" x2="1" y2="1.6" width="0.127" layer="21"/>
+</package>
+<package name="0805">
+<smd name="1" x="-1" y="0" dx="0.8" dy="1.3" layer="1"/>
+<smd name="2" x="1" y="0" dx="0.8" dy="1.3" layer="1"/>
+<text x="-0.762" y="0.8255" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.016" y="-2.032" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-1" y1="-0.6" x2="1" y2="0.6" layer="51"/>
+<rectangle x1="-0.4" y1="-0.5" x2="0.4" y2="0.5" layer="21"/>
+</package>
+<package name="0603-CAP">
+<wire x1="-0.356" y1="0.332" x2="0.356" y2="0.332" width="0.1016" layer="51"/>
+<wire x1="-0.356" y1="-0.319" x2="0.356" y2="-0.319" width="0.1016" layer="51"/>
+<smd name="1" x="-0.8" y="0" dx="0.8" dy="0.95" layer="1"/>
+<smd name="2" x="0.8" y="0" dx="0.8" dy="0.95" layer="1"/>
+<text x="-0.889" y="1.397" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.016" y="-2.413" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-0.8382" y1="-0.4" x2="-0.3381" y2="0.4" layer="51"/>
+<rectangle x1="0.3302" y1="-0.4" x2="0.8303" y2="0.4" layer="51"/>
+<rectangle x1="-0.2" y1="-0.3" x2="0.2" y2="0.3" layer="21"/>
+</package>
+<package name="1210">
+<wire x1="-1.6" y1="1.3" x2="1.6" y2="1.3" width="0.127" layer="51"/>
+<wire x1="1.6" y1="1.3" x2="1.6" y2="-1.3" width="0.127" layer="51"/>
+<wire x1="1.6" y1="-1.3" x2="-1.6" y2="-1.3" width="0.127" layer="51"/>
+<wire x1="-1.6" y1="-1.3" x2="-1.6" y2="1.3" width="0.127" layer="51"/>
+<wire x1="-1.6" y1="1.3" x2="1.6" y2="1.3" width="0.2032" layer="51"/>
+<wire x1="-1.6" y1="-1.3" x2="1.6" y2="-1.3" width="0.2032" layer="51"/>
+<smd name="1" x="-1.6" y="0" dx="1.2" dy="2.5" layer="1"/>
+<smd name="2" x="1.6" y="0" dx="1.2" dy="2.5" layer="1"/>
+<text x="-2.07" y="1.77" size="1.016" layer="25">&gt;NAME</text>
+<text x="-2.17" y="-3.24" size="1.016" layer="27">&gt;VALUE</text>
+</package>
+<package name="1206">
+<wire x1="-0.965" y1="0.787" x2="0.965" y2="0.787" width="0.1016" layer="51"/>
+<wire x1="-0.965" y1="-0.787" x2="0.965" y2="-0.787" width="0.1016" layer="51"/>
+<smd name="1" x="-1.4" y="0" dx="1.6" dy="1.8" layer="1"/>
+<smd name="2" x="1.4" y="0" dx="1.6" dy="1.8" layer="1"/>
+<text x="-1.27" y="1.143" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.397" y="-2.794" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-1.7018" y1="-0.8509" x2="-0.9517" y2="0.8491" layer="51"/>
+<rectangle x1="0.9517" y1="-0.8491" x2="1.7018" y2="0.8509" layer="51"/>
+<rectangle x1="-0.1999" y1="-0.4001" x2="0.1999" y2="0.4001" layer="35"/>
+</package>
+<package name="2220-C">
+<smd name="P$1" x="-2.6" y="0" dx="1.2" dy="5" layer="1"/>
+<smd name="P$2" x="2.6" y="0" dx="1.2" dy="5" layer="1"/>
+<text x="-1.5" y="3" size="0.6096" layer="125">&gt;NAME</text>
+<text x="-1.5" y="-3.5" size="0.6096" layer="127">&gt;VALUE</text>
+</package>
+<package name="0402">
+<description>&lt;b&gt;CAPACITOR&lt;/b&gt;&lt;p&gt;
+chip</description>
+<wire x1="-0.245" y1="0.224" x2="0.245" y2="0.224" width="0.1524" layer="51"/>
+<wire x1="0.245" y1="-0.224" x2="-0.245" y2="-0.224" width="0.1524" layer="51"/>
+<smd name="1" x="-0.525" y="0" dx="0.575" dy="0.7" layer="1"/>
+<smd name="2" x="0.525" y="0" dx="0.575" dy="0.7" layer="1"/>
+<text x="-0.889" y="0.6985" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.0795" y="-1.778" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-0.554" y1="-0.3048" x2="-0.254" y2="0.2951" layer="51"/>
+<rectangle x1="0.2588" y1="-0.3048" x2="0.5588" y2="0.2951" layer="51"/>
+</package>
+<package name="R2010">
+<description>&lt;b&gt;RESISTOR&lt;/b&gt;&lt;p&gt;
+chip</description>
+<wire x1="-1.662" y1="1.245" x2="1.662" y2="1.245" width="0.1524" layer="51"/>
+<wire x1="-1.637" y1="-1.245" x2="1.687" y2="-1.245" width="0.1524" layer="51"/>
+<wire x1="-3.473" y1="1.483" x2="3.473" y2="1.483" width="0.0508" layer="39"/>
+<wire x1="3.473" y1="1.483" x2="3.473" y2="-1.483" width="0.0508" layer="39"/>
+<wire x1="3.473" y1="-1.483" x2="-3.473" y2="-1.483" width="0.0508" layer="39"/>
+<wire x1="-3.473" y1="-1.483" x2="-3.473" y2="1.483" width="0.0508" layer="39"/>
+<wire x1="-1.027" y1="1.245" x2="1.027" y2="1.245" width="0.1524" layer="21"/>
+<wire x1="-1.002" y1="-1.245" x2="1.016" y2="-1.245" width="0.1524" layer="21"/>
+<smd name="1" x="-2.2" y="0" dx="1.8" dy="2.7" layer="1"/>
+<smd name="2" x="2.2" y="0" dx="1.8" dy="2.7" layer="1"/>
+<text x="-2.54" y="1.5875" size="1.016" layer="25">&gt;NAME</text>
+<text x="-2.54" y="-3.302" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-2.4892" y1="-1.3208" x2="-1.6393" y2="1.3292" layer="51"/>
+<rectangle x1="1.651" y1="-1.3208" x2="2.5009" y2="1.3292" layer="51"/>
+</package>
+<package name="0603-RES">
+<wire x1="-0.356" y1="0.432" x2="0.356" y2="0.432" width="0.1016" layer="51"/>
+<wire x1="-0.356" y1="-0.419" x2="0.356" y2="-0.419" width="0.1016" layer="51"/>
+<smd name="1" x="-0.85" y="0" dx="0.6" dy="0.9" layer="1"/>
+<smd name="2" x="0.85" y="0" dx="0.6" dy="0.9" layer="1"/>
+<text x="0" y="1" size="0.8128" layer="25" font="vector" align="center">&gt;NAME</text>
+<text x="0" y="-1" size="0.8128" layer="27" font="vector" ratio="10" align="center">&gt;VALUE</text>
+<rectangle x1="-0.8382" y1="-0.4699" x2="-0.3381" y2="0.4801" layer="51"/>
+<rectangle x1="0.3302" y1="-0.4699" x2="0.8303" y2="0.4801" layer="51"/>
+<rectangle x1="-0.1999" y1="-0.3" x2="0.1999" y2="0.3" layer="35"/>
+<rectangle x1="-0.2286" y1="-0.381" x2="0.2286" y2="0.381" layer="21"/>
+</package>
+<package name="R2512">
+<wire x1="-2.362" y1="1.473" x2="2.387" y2="1.473" width="0.1524" layer="51"/>
+<wire x1="-2.362" y1="-1.473" x2="2.387" y2="-1.473" width="0.1524" layer="51"/>
+<smd name="1" x="-2.8" y="0" dx="1.8" dy="3.2" layer="1"/>
+<smd name="2" x="2.8" y="0" dx="1.8" dy="3.2" layer="1"/>
+<text x="-2.54" y="1.905" size="1.016" layer="25">&gt;NAME</text>
+<text x="-2.54" y="-3.175" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-3.2004" y1="-1.5494" x2="-2.3505" y2="1.5507" layer="51"/>
+<rectangle x1="2.3622" y1="-1.5494" x2="3.2121" y2="1.5507" layer="51"/>
+</package>
+<package name="TO220ACS">
+<description>&lt;B&gt;DIODE&lt;/B&gt;&lt;p&gt;
+2-lead molded, vertical</description>
+<wire x1="5.08" y1="-1.143" x2="4.953" y2="-4.064" width="0.1524" layer="21"/>
+<wire x1="4.699" y1="-4.318" x2="4.953" y2="-4.064" width="0.1524" layer="21"/>
+<wire x1="4.699" y1="-4.318" x2="-4.699" y2="-4.318" width="0.1524" layer="21"/>
+<wire x1="-4.953" y1="-4.064" x2="-4.699" y2="-4.318" width="0.1524" layer="21"/>
+<wire x1="-4.953" y1="-4.064" x2="-5.08" y2="-1.143" width="0.1524" layer="21"/>
+<circle x="-4.4958" y="-3.7084" radius="0.254" width="0" layer="21"/>
+<pad name="C" x="-2.54" y="-2.54" drill="1.016" shape="long" rot="R90"/>
+<pad name="A" x="2.54" y="-2.54" drill="1.016" shape="long" rot="R90"/>
+<text x="-5.08" y="-6.0452" size="1.016" layer="25" ratio="10">&gt;NAME</text>
+<text x="-5.08" y="-7.62" size="1.016" layer="27" ratio="10">&gt;VALUE</text>
+<rectangle x1="-5.334" y1="-0.762" x2="5.334" y2="0" layer="21"/>
+<rectangle x1="-5.334" y1="-1.27" x2="-3.429" y2="-0.762" layer="21"/>
+<rectangle x1="-3.429" y1="-1.27" x2="-1.651" y2="-0.762" layer="51"/>
+<rectangle x1="3.429" y1="-1.27" x2="5.334" y2="-0.762" layer="21"/>
+<rectangle x1="1.651" y1="-1.27" x2="3.429" y2="-0.762" layer="51"/>
+<rectangle x1="-1.651" y1="-1.27" x2="1.651" y2="-0.762" layer="21"/>
+</package>
+</packages>
+<symbols>
+<symbol name="TS2">
+<wire x1="0" y1="1.905" x2="0" y2="2.54" width="0.254" layer="94"/>
+<wire x1="-4.445" y1="1.905" x2="-3.175" y2="1.905" width="0.254" layer="94"/>
+<wire x1="-4.445" y1="-1.905" x2="-3.175" y2="-1.905" width="0.254" layer="94"/>
+<wire x1="-4.445" y1="1.905" x2="-4.445" y2="0" width="0.254" layer="94"/>
+<wire x1="-4.445" y1="0" x2="-4.445" y2="-1.905" width="0.254" layer="94"/>
+<wire x1="-2.54" y1="0" x2="-1.905" y2="0" width="0.1524" layer="94"/>
+<wire x1="-1.27" y1="0" x2="-0.635" y2="0" width="0.1524" layer="94"/>
+<wire x1="-4.445" y1="0" x2="-3.175" y2="0" width="0.1524" layer="94"/>
+<wire x1="2.54" y1="2.54" x2="0" y2="2.54" width="0.1524" layer="94"/>
+<wire x1="2.54" y1="-2.54" x2="0" y2="-2.54" width="0.1524" layer="94"/>
+<wire x1="0" y1="-2.54" x2="-1.27" y2="1.905" width="0.254" layer="94"/>
+<circle x="0" y="-2.54" radius="0.127" width="0.4064" layer="94"/>
+<circle x="0" y="2.54" radius="0.127" width="0.4064" layer="94"/>
+<text x="-6.35" y="-2.54" size="1.778" layer="95" rot="R90">&gt;NAME</text>
+<text x="-3.81" y="3.175" size="1.778" layer="96" rot="R90">&gt;VALUE</text>
+<pin name="P" x="0" y="-5.08" visible="pad" length="short" direction="pas" swaplevel="2" rot="R90"/>
+<pin name="S" x="0" y="5.08" visible="pad" length="short" direction="pas" swaplevel="1" rot="R270"/>
+<pin name="S1" x="2.54" y="5.08" visible="pad" length="short" direction="pas" swaplevel="1" rot="R270"/>
+<pin name="P1" x="2.54" y="-5.08" visible="pad" length="short" direction="pas" swaplevel="2" rot="R90"/>
+</symbol>
+<symbol name="CAP">
+<wire x1="0" y1="2.54" x2="0" y2="2.032" width="0.1524" layer="94"/>
+<wire x1="0" y1="0" x2="0" y2="0.508" width="0.1524" layer="94"/>
+<text x="1.524" y="2.921" size="1.778" layer="95">&gt;NAME</text>
+<text x="1.524" y="-2.159" size="1.778" layer="96">&gt;VALUE</text>
+<rectangle x1="-2.032" y1="0.508" x2="2.032" y2="1.016" layer="94"/>
+<rectangle x1="-2.032" y1="1.524" x2="2.032" y2="2.032" layer="94"/>
+<pin name="1" x="0" y="5.08" visible="off" length="short" direction="pas" swaplevel="1" rot="R270"/>
+<pin name="2" x="0" y="-2.54" visible="off" length="short" direction="pas" swaplevel="1" rot="R90"/>
+<text x="1.524" y="-4.064" size="1.27" layer="97">&gt;PACKAGE</text>
+<text x="1.524" y="-5.842" size="1.27" layer="97">&gt;VOLTAGE</text>
+<text x="1.524" y="-7.62" size="1.27" layer="97">&gt;TYPE</text>
+</symbol>
+<symbol name="RESISTOR">
+<wire x1="-2.54" y1="0" x2="-2.159" y2="1.016" width="0.1524" layer="94"/>
+<wire x1="-2.159" y1="1.016" x2="-1.524" y2="-1.016" width="0.1524" layer="94"/>
+<wire x1="-1.524" y1="-1.016" x2="-0.889" y2="1.016" width="0.1524" layer="94"/>
+<wire x1="-0.889" y1="1.016" x2="-0.254" y2="-1.016" width="0.1524" layer="94"/>
+<wire x1="-0.254" y1="-1.016" x2="0.381" y2="1.016" width="0.1524" layer="94"/>
+<wire x1="0.381" y1="1.016" x2="1.016" y2="-1.016" width="0.1524" layer="94"/>
+<wire x1="1.016" y1="-1.016" x2="1.651" y2="1.016" width="0.1524" layer="94"/>
+<wire x1="1.651" y1="1.016" x2="2.286" y2="-1.016" width="0.1524" layer="94"/>
+<wire x1="2.286" y1="-1.016" x2="2.54" y2="0" width="0.1524" layer="94"/>
+<text x="-3.81" y="1.4986" size="1.778" layer="95">&gt;NAME</text>
+<text x="-3.81" y="-3.302" size="1.778" layer="96">&gt;VALUE</text>
+<pin name="2" x="5.08" y="0" visible="off" length="short" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="1" x="-5.08" y="0" visible="off" length="short" direction="pas" swaplevel="1"/>
+<text x="-3.81" y="-6.858" size="1.27" layer="97">&gt;PRECISION</text>
+<text x="-3.81" y="-5.08" size="1.27" layer="97">&gt;PACKAGE</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="2-8X4-5_SWITCH" prefix="S">
+<gates>
+<gate name="G$1" symbol="TS2" x="0" y="0"/>
+</gates>
+<devices>
+<device name="" package="TACT-SWITCH-KMR6">
+<connects>
+<connect gate="G$1" pin="P" pad="P$1"/>
+<connect gate="G$1" pin="P1" pad="P$2"/>
+<connect gate="G$1" pin="S" pad="P$3"/>
+<connect gate="G$1" pin="S1" pad="P$4"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="SIDE" package="TACT-SWITCH-SIDE">
+<connects>
+<connect gate="G$1" pin="P" pad="P$1"/>
+<connect gate="G$1" pin="P1" pad="P$2"/>
+<connect gate="G$1" pin="S" pad="P$3"/>
+<connect gate="G$1" pin="S1" pad="P$4"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+<deviceset name="CAP" prefix="C" uservalue="yes">
+<description>&lt;b&gt;Capacitor&lt;/b&gt;
+Standard 0603 ceramic capacitor, and 0.1" leaded capacitor.</description>
+<gates>
+<gate name="G$1" symbol="CAP" x="0" y="0"/>
+</gates>
+<devices>
+<device name="0805" package="0805">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="0805"/>
+<attribute name="TYPE" value="" constant="no"/>
+<attribute name="VOLTAGE" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="0603-CAP" package="0603-CAP">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="0603"/>
+<attribute name="TYPE" value="" constant="no"/>
+<attribute name="VOLTAGE" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="1210" package="1210">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="1210" constant="no"/>
+<attribute name="TYPE" value="" constant="no"/>
+<attribute name="VOLTAGE" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="1206" package="1206">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="1206" constant="no"/>
+<attribute name="TYPE" value="" constant="no"/>
+<attribute name="VOLTAGE" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="2220" package="2220-C">
+<connects>
+<connect gate="G$1" pin="1" pad="P$1"/>
+<connect gate="G$1" pin="2" pad="P$2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0402" package="0402">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+<deviceset name="RESISTOR" prefix="R" uservalue="yes">
+<description>&lt;b&gt;Resistor&lt;/b&gt;
+Basic schematic elements and footprints for 0603, 1206, and PTH resistors.</description>
+<gates>
+<gate name="G$1" symbol="RESISTOR" x="0" y="0"/>
+</gates>
+<devices>
+<device name="1206" package="1206">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="1206" constant="no"/>
+<attribute name="PRECISION" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="2010" package="R2010">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="2010"/>
+<attribute name="PRECISION" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="0805-RES" package="0805">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="0805"/>
+<attribute name="PRECISION" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="0603-RES" package="0603-RES">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="0603"/>
+<attribute name="PRECISION" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="2512" package="R2512">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PACKAGE" value="2512"/>
+<attribute name="PRECISION" value="" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="TO220ACS" package="TO220ACS">
+<connects>
+<connect gate="G$1" pin="1" pad="A"/>
+<connect gate="G$1" pin="2" pad="C"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0402" package="0402">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="2" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="comm">
+<packages>
+<package name="8-MSOP">
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="21"/>
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="1.5" y2="1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="-1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="-1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="1.5" y1="1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<text x="-2.5" y="-2" size="0.8128" layer="27" font="vector" align="top-left">&gt;VALUE</text>
+<text x="-2.5" y="2" size="0.8128" layer="25" font="vector">&gt;NAME</text>
+<smd name="1" x="-2.2" y="0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="2" x="-2.2" y="0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="3" x="-2.2" y="-0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="4" x="-2.2" y="-0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="5" x="2.2" y="-0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="6" x="2.2" y="-0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="7" x="2.2" y="0.325" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+<smd name="8" x="2.2" y="0.975" dx="1.4" dy="0.4" layer="1" roundness="25"/>
+</package>
+<package name="8-MSOP-FAB">
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="21"/>
+<circle x="-2" y="1.75" radius="0.1" width="0.2" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="1.5" y2="1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="-1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="-1.5" y1="1.5" x2="-1.5" y2="-1.5" width="0.127" layer="51"/>
+<wire x1="1.5" y1="1.5" x2="1.5" y2="-1.5" width="0.127" layer="51"/>
+<text x="-2.5" y="-2" size="0.8128" layer="27" font="vector" align="top-left">&gt;VALUE</text>
+<text x="-2.5" y="2" size="0.8128" layer="25" font="vector">&gt;NAME</text>
+<smd name="1" x="-2.2" y="0.975" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="2" x="-2.2" y="0.325" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="3" x="-2.2" y="-0.325" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="4" x="-2.2" y="-0.975" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="5" x="2.2" y="-0.975" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="6" x="2.2" y="-0.325" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="7" x="2.2" y="0.325" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+<smd name="8" x="2.2" y="0.975" dx="1.4" dy="0.22" layer="1" roundness="25"/>
+</package>
+</packages>
+<symbols>
+<symbol name="RS485-ISL83078E">
+<pin name="!RE" x="-15.24" y="5.08" length="middle"/>
+<pin name="RO" x="-15.24" y="7.62" length="middle"/>
+<pin name="DE" x="-15.24" y="-7.62" length="middle"/>
+<pin name="DI" x="-15.24" y="-5.08" length="middle"/>
+<pin name="GND" x="15.24" y="-7.62" length="middle" rot="R180"/>
+<pin name="VCC" x="15.24" y="0" length="middle" rot="R180"/>
+<pin name="B/Z" x="15.24" y="7.62" length="middle" rot="R180"/>
+<pin name="A/Y" x="15.24" y="5.08" length="middle" rot="R180"/>
+<wire x1="10.16" y1="-10.16" x2="10.16" y2="10.16" width="0.254" layer="94"/>
+<wire x1="10.16" y1="10.16" x2="-10.16" y2="10.16" width="0.254" layer="94"/>
+<wire x1="-10.16" y1="10.16" x2="-10.16" y2="-10.16" width="0.254" layer="94"/>
+<wire x1="-10.16" y1="-10.16" x2="10.16" y2="-10.16" width="0.254" layer="94"/>
+<text x="-10.16" y="12.7" size="1.27" layer="95" align="top-left">&gt;NAME</text>
+<text x="-10.16" y="-12.7" size="1.27" layer="95">&gt;VALUE</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="RS485-ISL83078E" prefix="U">
+<gates>
+<gate name="G$1" symbol="RS485-ISL83078E" x="0" y="0"/>
+</gates>
+<devices>
+<device name="MSOP" package="8-MSOP">
+<connects>
+<connect gate="G$1" pin="!RE" pad="2"/>
+<connect gate="G$1" pin="A/Y" pad="6"/>
+<connect gate="G$1" pin="B/Z" pad="7"/>
+<connect gate="G$1" pin="DE" pad="3"/>
+<connect gate="G$1" pin="DI" pad="4"/>
+<connect gate="G$1" pin="GND" pad="5"/>
+<connect gate="G$1" pin="RO" pad="1"/>
+<connect gate="G$1" pin="VCC" pad="8"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="MSOP-FAB" package="8-MSOP-FAB">
+<connects>
+<connect gate="G$1" pin="!RE" pad="2"/>
+<connect gate="G$1" pin="A/Y" pad="6"/>
+<connect gate="G$1" pin="B/Z" pad="7"/>
+<connect gate="G$1" pin="DE" pad="3"/>
+<connect gate="G$1" pin="DI" pad="4"/>
+<connect gate="G$1" pin="GND" pad="5"/>
+<connect gate="G$1" pin="RO" pad="1"/>
+<connect gate="G$1" pin="VCC" pad="8"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="connector">
+<packages>
+<package name="DX4R005HJ5_100">
+<wire x1="3.25" y1="-2.6" x2="-3.25" y2="-2.6" width="0.127" layer="21"/>
+<wire x1="-3.25" y1="2.6" x2="-3.25" y2="0" width="0.127" layer="51"/>
+<wire x1="3.25" y1="2.6" x2="3.25" y2="0" width="0.127" layer="51"/>
+<wire x1="-1.75" y1="2.6" x2="1.75" y2="2.6" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="-2.2" x2="-3.25" y2="-2.6" width="0.127" layer="51"/>
+<wire x1="3.25" y1="-2.6" x2="3.25" y2="-2.2" width="0.127" layer="51"/>
+<smd name="GND@3" x="-2.175" y="-1.1" dx="2.15" dy="1.9" layer="1"/>
+<smd name="GND@4" x="2.175" y="-1.1" dx="2.15" dy="1.9" layer="1"/>
+<smd name="GND@1" x="-2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="GND@2" x="2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="D+" x="0" y="1.6" dx="0.35" dy="1.35" layer="1"/>
+<smd name="D-" x="-0.65" y="1.6" dx="0.35" dy="1.35" layer="1"/>
+<smd name="VBUS" x="-1.3" y="1.6" dx="0.35" dy="1.35" layer="1"/>
+<smd name="ID" x="0.65" y="1.6" dx="0.35" dy="1.35" layer="1"/>
+<smd name="GND" x="1.3" y="1.6" dx="0.35" dy="1.35" layer="1"/>
+<text x="4.1275" y="-1.5875" size="0.6096" layer="27" font="vector" rot="R90">&gt;Value</text>
+<text x="-3.4925" y="-1.27" size="0.6096" layer="25" font="vector" rot="R90">&gt;Name</text>
+</package>
+<package name="DX4R005HJ5">
+<wire x1="3.25" y1="-2.6" x2="-3.25" y2="-2.6" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="2.6" x2="-3.25" y2="0" width="0.127" layer="21"/>
+<wire x1="3.25" y1="2.6" x2="3.25" y2="0" width="0.127" layer="21"/>
+<wire x1="-1.75" y1="2.6" x2="1.75" y2="2.6" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="-2.2" x2="-3.25" y2="-2.6" width="0.127" layer="51"/>
+<wire x1="3.25" y1="-2.6" x2="3.25" y2="-2.2" width="0.127" layer="51"/>
+<smd name="GND@3" x="-2.475" y="-1.1" dx="2.75" dy="1.9" layer="1"/>
+<smd name="GND@4" x="2.475" y="-1.1" dx="2.75" dy="1.9" layer="1"/>
+<smd name="GND@1" x="-2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="GND@2" x="2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="D+" x="0" y="1.9" dx="0.4" dy="1.95" layer="1"/>
+<smd name="D-" x="-0.65" y="1.9" dx="0.4" dy="1.95" layer="1"/>
+<smd name="VBUS" x="-1.3" y="1.9" dx="0.4" dy="1.95" layer="1"/>
+<smd name="ID" x="0.65" y="1.9" dx="0.4" dy="1.95" layer="1"/>
+<smd name="GND" x="1.3" y="1.9" dx="0.4" dy="1.95" layer="1"/>
+<text x="-3.4925" y="-1.27" size="0.6096" layer="25" font="vector" rot="R90">&gt;Name</text>
+<text x="4.1275" y="-1.5875" size="0.6096" layer="25" font="vector" rot="R90">&gt;Value</text>
+</package>
+<package name="DX4R005HJ5_64">
+<wire x1="3.25" y1="-2.6" x2="-3.25" y2="-2.6" width="0.127" layer="21"/>
+<wire x1="-3.25" y1="2.6" x2="-3.25" y2="0" width="0.127" layer="51"/>
+<wire x1="3.25" y1="2.6" x2="3.25" y2="0" width="0.127" layer="51"/>
+<wire x1="-1.75" y1="2.6" x2="1.75" y2="2.6" width="0.127" layer="51"/>
+<wire x1="-3.25" y1="-2.2" x2="-3.25" y2="-2.6" width="0.127" layer="51"/>
+<wire x1="3.25" y1="-2.6" x2="3.25" y2="-2.2" width="0.127" layer="51"/>
+<smd name="GND@3" x="-2.175" y="-1.1" dx="2.15" dy="1.9" layer="1"/>
+<smd name="GND@4" x="2.175" y="-1.1" dx="2.15" dy="1.9" layer="1"/>
+<smd name="GND@1" x="-2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="GND@2" x="2.5" y="1.95" dx="1.2" dy="1.3" layer="1"/>
+<smd name="D+" x="0" y="1.6" dx="0.21" dy="1.35" layer="1"/>
+<smd name="D-" x="-0.65" y="1.6" dx="0.21" dy="1.35" layer="1"/>
+<smd name="VBUS" x="-1.3" y="1.6" dx="0.21" dy="1.35" layer="1"/>
+<smd name="ID" x="0.65" y="1.6" dx="0.21" dy="1.35" layer="1"/>
+<smd name="GND" x="1.3" y="1.6" dx="0.21" dy="1.35" layer="1"/>
+<text x="-3.4925" y="-1.27" size="0.6096" layer="25" font="vector" rot="R90">&gt;Name</text>
+<text x="4.1275" y="-1.5875" size="0.6096" layer="27" font="vector" rot="R90">&gt;Value</text>
+</package>
+<package name="USB_MICRO_609-4613-1-ND">
+<smd name="HD0" x="-3.8" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD4" x="-3.1" y="2.55" dx="2.1" dy="1.6" layer="1"/>
+<smd name="HD5" x="3.1" y="2.55" dx="2.1" dy="1.6" layer="1"/>
+<smd name="D+" x="0" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="D-" x="-0.65" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="VBUS" x="-1.3" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="ID" x="0.65" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<smd name="GND" x="1.3" y="2.675" dx="0.4" dy="1.35" layer="1"/>
+<text x="4.9275" y="1.2125" size="0.6096" layer="27" font="vector" rot="R90">&gt;Value</text>
+<text x="-4.3925" y="1.13" size="0.6096" layer="25" font="vector" rot="R90">&gt;Name</text>
+<smd name="HD1" x="-1.05" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD2" x="1.05" y="0" dx="1.9" dy="1.8" layer="1"/>
+<smd name="HD3" x="3.8" y="0" dx="1.9" dy="1.8" layer="1"/>
+<wire x1="-4.7" y1="-1.45" x2="4.7" y2="-1.45" width="0.127" layer="51"/>
+<text x="0" y="-1.3" size="0.8128" layer="51" font="vector" align="bottom-center">\\ PCB Edge /</text>
+<wire x1="-3.9" y1="3" x2="-3.9" y2="-2.5" width="0.127" layer="51"/>
+<wire x1="-3.9" y1="-2.5" x2="3.9" y2="-2.5" width="0.127" layer="51"/>
+<wire x1="3.9" y1="-2.5" x2="3.9" y2="3" width="0.127" layer="51"/>
+<wire x1="3.9" y1="3" x2="-3.9" y2="3" width="0.127" layer="51"/>
+<wire x1="-3.9" y1="1.1" x2="-3.9" y2="1.5" width="0.127" layer="21"/>
+<wire x1="3.9" y1="1.1" x2="3.9" y2="1.5" width="0.127" layer="21"/>
+<wire x1="1.8" y1="3" x2="1.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-1.7" y1="3" x2="-1.8" y2="3" width="0.127" layer="21"/>
+<wire x1="4.4" y1="3" x2="4.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-4.4" y1="3" x2="-4.7" y2="3" width="0.127" layer="21"/>
+<wire x1="-3.9" y1="3.6" x2="-3.9" y2="3.8" width="0.127" layer="21"/>
+<wire x1="3.9" y1="3.6" x2="3.9" y2="3.8" width="0.127" layer="21"/>
+</package>
+<package name="MOLEX_1051330021">
+<smd name="D+" x="0" y="0" dx="0.4" dy="1.5" layer="1"/>
+<smd name="D-" x="-0.65" y="0" dx="0.4" dy="1.5" layer="1"/>
+<smd name="VBUS" x="-1.3" y="0" dx="0.4" dy="1.5" layer="1"/>
+<smd name="ID" x="0.65" y="0" dx="0.4" dy="1.5" layer="1"/>
+<smd name="GND" x="1.3" y="0" dx="0.4" dy="1.5" layer="1"/>
+<text x="-4.9" y="-0.5" size="0.6096" layer="25" font="vector" rot="R90" align="center">&gt;Name</text>
+<text x="4.8" y="-0.5" size="0.6096" layer="25" font="vector" rot="R90" align="center">&gt;Value</text>
+<pad name="P$1" x="-2.8" y="0.25" drill="0.85" shape="square"/>
+<pad name="P$2" x="2.8" y="0.25" drill="0.85" shape="square"/>
+<pad name="P$3" x="0" y="-1.9" drill="1.25" shape="square"/>
+<wire x1="-4.3" y1="1.4" x2="-4.3" y2="-2.4" width="0.1524" layer="51"/>
+<wire x1="-4.3" y1="-2.4" x2="4.3" y2="-2.4" width="0.1524" layer="51"/>
+<wire x1="4.3" y1="-2.4" x2="4.3" y2="1.4" width="0.1524" layer="51"/>
+<wire x1="4.3" y1="1.4" x2="-4.3" y2="1.4" width="0.1524" layer="51"/>
+</package>
+</packages>
+<symbols>
+<symbol name="USB-1">
+<wire x1="6.35" y1="-2.54" x2="6.35" y2="2.54" width="0.254" layer="94"/>
+<wire x1="6.35" y1="2.54" x2="-3.81" y2="2.54" width="0.254" layer="94"/>
+<wire x1="-3.81" y1="2.54" x2="-3.81" y2="-2.54" width="0.254" layer="94"/>
+<text x="-2.54" y="-1.27" size="2.54" layer="94">USB</text>
+<text x="-4.445" y="-1.905" size="1.27" layer="95" font="vector" rot="R90">&gt;Name</text>
+<text x="8.255" y="-1.905" size="1.27" layer="96" font="vector" rot="R90">&gt;Value</text>
+<pin name="D+" x="5.08" y="5.08" visible="pad" length="short" rot="R270"/>
+<pin name="D-" x="2.54" y="5.08" visible="pad" length="short" rot="R270"/>
+<pin name="VBUS" x="0" y="5.08" visible="pad" length="short" rot="R270"/>
+<pin name="GND" x="-2.54" y="5.08" visible="pad" length="short" rot="R270"/>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="USB" prefix="X">
+<description>SMD micro USB connector as found in the fablab inventory. 
+Three footprint variants included: 
+&lt;ol&gt;
+&lt;li&gt;609-4613-1-ND used by Jake
+&lt;li&gt; original, as described by manufacturer's datasheet
+&lt;li&gt; for milling with the 1/100" bit
+&lt;li&gt; for milling with the 1/64" bit
+&lt;/ol&gt;
+&lt;p&gt;Made by Zaerc.</description>
+<gates>
+<gate name="G$1" symbol="USB-1" x="0" y="0"/>
+</gates>
+<devices>
+<device name="_1/100" package="DX4R005HJ5_100">
+<connects>
+<connect gate="G$1" pin="D+" pad="D+"/>
+<connect gate="G$1" pin="D-" pad="D-"/>
+<connect gate="G$1" pin="GND" pad="GND"/>
+<connect gate="G$1" pin="VBUS" pad="VBUS"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="_ORIG" package="DX4R005HJ5">
+<connects>
+<connect gate="G$1" pin="D+" pad="D+"/>
+<connect gate="G$1" pin="D-" pad="D-"/>
+<connect gate="G$1" pin="GND" pad="GND"/>
+<connect gate="G$1" pin="VBUS" pad="VBUS"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="_1/64" package="DX4R005HJ5_64">
+<connects>
+<connect gate="G$1" pin="D+" pad="D+"/>
+<connect gate="G$1" pin="D-" pad="D-"/>
+<connect gate="G$1" pin="GND" pad="GND"/>
+<connect gate="G$1" pin="VBUS" pad="VBUS"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="" package="USB_MICRO_609-4613-1-ND">
+<connects>
+<connect gate="G$1" pin="D+" pad="D+"/>
+<connect gate="G$1" pin="D-" pad="D-"/>
+<connect gate="G$1" pin="GND" pad="GND"/>
+<connect gate="G$1" pin="VBUS" pad="VBUS"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="VERT" package="MOLEX_1051330021">
+<connects>
+<connect gate="G$1" pin="D+" pad="D+"/>
+<connect gate="G$1" pin="D-" pad="D-"/>
+<connect gate="G$1" pin="GND" pad="GND"/>
+<connect gate="G$1" pin="VBUS" pad="VBUS"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="power">
+<packages>
+<package name="SOT23-5">
+<description>&lt;b&gt;Small Outline Transistor&lt;/b&gt;, 5 lead</description>
+<wire x1="-1.544" y1="0.713" x2="1.544" y2="0.713" width="0.1524" layer="51"/>
+<wire x1="1.544" y1="0.713" x2="1.544" y2="-0.712" width="0.1524" layer="51"/>
+<wire x1="1.544" y1="-0.712" x2="-1.544" y2="-0.712" width="0.1524" layer="51"/>
+<wire x1="-1.544" y1="-0.712" x2="-1.544" y2="0.713" width="0.1524" layer="51"/>
+<smd name="5" x="-0.95" y="1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="4" x="0.95" y="1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="1" x="-0.95" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="2" x="0" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<smd name="3" x="0.95" y="-1.306" dx="0.5334" dy="1.1938" layer="1"/>
+<text x="-1.778" y="-1.778" size="1.27" layer="25" ratio="10" rot="R90">&gt;NAME</text>
+<text x="3.048" y="-1.778" size="1.27" layer="27" ratio="10" rot="R90">&gt;VALUE</text>
+<rectangle x1="-1.1875" y1="0.7126" x2="-0.7125" y2="1.5439" layer="51"/>
+<rectangle x1="0.7125" y1="0.7126" x2="1.1875" y2="1.5439" layer="51"/>
+<rectangle x1="-1.1875" y1="-1.5437" x2="-0.7125" y2="-0.7124" layer="51"/>
+<rectangle x1="-0.2375" y1="-1.5437" x2="0.2375" y2="-0.7124" layer="51"/>
+<rectangle x1="0.7125" y1="-1.5437" x2="1.1875" y2="-0.7124" layer="51"/>
+<wire x1="-1.5" y1="-1.9" x2="-1.5" y2="-1.2" width="0.127" layer="21"/>
+</package>
+</packages>
+<symbols>
+<symbol name="VREG-AP2112">
+<pin name="VIN" x="-12.7" y="2.54" length="middle"/>
+<pin name="EN" x="-12.7" y="-2.54" length="middle"/>
+<pin name="GND" x="0" y="-10.16" length="middle" rot="R90"/>
+<pin name="VOUT" x="12.7" y="2.54" length="middle" rot="R180"/>
+<wire x1="-7.62" y1="5.08" x2="-7.62" y2="-5.08" width="0.254" layer="94"/>
+<wire x1="-7.62" y1="-5.08" x2="7.62" y2="-5.08" width="0.254" layer="94"/>
+<wire x1="7.62" y1="-5.08" x2="7.62" y2="5.08" width="0.254" layer="94"/>
+<wire x1="7.62" y1="5.08" x2="-7.62" y2="5.08" width="0.254" layer="94"/>
+<text x="-2.54" y="7.62" size="1.27" layer="95">&gt;NAME</text>
+<text x="2.54" y="-7.62" size="1.27" layer="96">&gt;VALUE</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="VREG-AP2112" prefix="U">
+<gates>
+<gate name="G$1" symbol="VREG-AP2112" x="0" y="0"/>
+</gates>
+<devices>
+<device name="" package="SOT23-5">
+<connects>
+<connect gate="G$1" pin="EN" pad="3"/>
+<connect gate="G$1" pin="GND" pad="2"/>
+<connect gate="G$1" pin="VIN" pad="1"/>
+<connect gate="G$1" pin="VOUT" pad="5"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="SparkFun-Connectors">
+<description>&lt;h3&gt;SparkFun Connectors&lt;/h3&gt;
+This library contains electrically-functional connectors. 
+&lt;br&gt;
+&lt;br&gt;
+We've spent an enormous amount of time creating and checking these footprints and parts, but it is &lt;b&gt; the end user's responsibility&lt;/b&gt; to ensure correctness and suitablity for a given componet or application. 
+&lt;br&gt;
+&lt;br&gt;If you enjoy using this library, please buy one of our products at &lt;a href=" www.sparkfun.com"&gt;SparkFun.com&lt;/a&gt;.
+&lt;br&gt;
+&lt;br&gt;
+&lt;b&gt;Licensing:&lt;/b&gt; Creative Commons ShareAlike 4.0 International - https://creativecommons.org/licenses/by-sa/4.0/ 
+&lt;br&gt;
+&lt;br&gt;
+You are welcome to use this library for commercial purposes. For attribution, we ask that when you begin to sell your device using our footprint, you email us with a link to the product being sold. We want bragging rights that we helped (in a very small part) to create your 8th world wonder. We would like the opportunity to feature your device on our homepage.</description>
+<packages>
+<package name="2X5">
+<description>&lt;h3&gt;Plated Through Hole - 2x5&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-1.27" y1="-0.635" x2="-0.635" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="0.635" y1="-1.27" x2="1.27" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="1.27" y1="-0.635" x2="1.905" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="3.175" y1="-1.27" x2="3.81" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="3.81" y1="-0.635" x2="4.445" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="5.715" y1="-1.27" x2="6.35" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="6.35" y1="-0.635" x2="6.985" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="8.255" y1="-1.27" x2="8.89" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="-1.27" y1="-0.635" x2="-1.27" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="-1.27" y1="3.175" x2="-0.635" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="-0.635" y1="3.81" x2="0.635" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="0.635" y1="3.81" x2="1.27" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="1.27" y1="3.175" x2="1.905" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="1.905" y1="3.81" x2="3.175" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="3.175" y1="3.81" x2="3.81" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="3.81" y1="3.175" x2="4.445" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="4.445" y1="3.81" x2="5.715" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="5.715" y1="3.81" x2="6.35" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="6.35" y1="3.175" x2="6.985" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="6.985" y1="3.81" x2="8.255" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="8.255" y1="3.81" x2="8.89" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="1.27" y1="3.175" x2="1.27" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="3.81" y1="3.175" x2="3.81" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="6.35" y1="3.175" x2="6.35" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="8.89" y1="3.175" x2="8.89" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="6.985" y1="-1.27" x2="8.255" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="4.445" y1="-1.27" x2="5.715" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="1.905" y1="-1.27" x2="3.175" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="-0.635" y1="-1.27" x2="0.635" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="8.89" y1="-0.635" x2="9.525" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="10.795" y1="-1.27" x2="11.43" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="8.89" y1="3.175" x2="9.525" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="9.525" y1="3.81" x2="10.795" y2="3.81" width="0.1524" layer="21"/>
+<wire x1="10.795" y1="3.81" x2="11.43" y2="3.175" width="0.1524" layer="21"/>
+<wire x1="11.43" y1="3.175" x2="11.43" y2="-0.635" width="0.1524" layer="21"/>
+<wire x1="9.525" y1="-1.27" x2="10.795" y2="-1.27" width="0.1524" layer="21"/>
+<wire x1="-0.635" y1="-1.651" x2="0.635" y2="-1.651" width="0.2032" layer="21"/>
+<pad name="1" x="0" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="2" x="0" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="3" x="2.54" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="4" x="2.54" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="5" x="5.08" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="6" x="5.08" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="7" x="7.62" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="8" x="7.62" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="9" x="10.16" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="10" x="10.16" y="2.54" drill="1.016" diameter="1.8796"/>
+<rectangle x1="-0.254" y1="-0.254" x2="0.254" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="-0.254" y1="2.286" x2="0.254" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="2.286" y1="2.286" x2="2.794" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="2.286" y1="-0.254" x2="2.794" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="4.826" y1="2.286" x2="5.334" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="4.826" y1="-0.254" x2="5.334" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="9.906" y1="2.286" x2="10.414" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="9.906" y1="-0.254" x2="10.414" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="7.366" y1="-0.254" x2="7.874" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="7.366" y1="2.286" x2="7.874" y2="2.794" layer="51" rot="R90"/>
+<text x="-1.27" y="3.937" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-1.27" y="-2.54" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-0.635" y1="-1.651" x2="0.635" y2="-1.651" width="0.2032" layer="22"/>
+</package>
+<package name="2X5-RA">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Right Angle Male Headers&lt;/h3&gt;
+tDocu shows pin location. 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.54" y1="5.715" x2="-2.54" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="2.8" y1="6.3" x2="5.3" y2="6.3" width="0.2032" layer="21"/>
+<wire x1="5.3" y1="6.3" x2="5.3" y2="-6.3" width="0.2032" layer="21"/>
+<wire x1="5.3" y1="-6.3" x2="2.8" y2="-6.3" width="0.2032" layer="21"/>
+<wire x1="2.8" y1="-6.3" x2="2.8" y2="6.3" width="0.2032" layer="21"/>
+<wire x1="5.3" y1="0" x2="11.3" y2="0" width="0.127" layer="51"/>
+<wire x1="5.3" y1="-2.54" x2="11.3" y2="-2.54" width="0.127" layer="51"/>
+<wire x1="5.3" y1="-5.08" x2="11.3" y2="-5.08" width="0.127" layer="51"/>
+<wire x1="5.3" y1="2.54" x2="11.3" y2="2.54" width="0.127" layer="51"/>
+<wire x1="5.3" y1="5.08" x2="11.3" y2="5.08" width="0.127" layer="51"/>
+<wire x1="8.2" y1="7" x2="8.2" y2="-6.9" width="0.127" layer="51"/>
+<wire x1="13.8" y1="6.3" x2="13.8" y2="-6.3" width="0.127" layer="51"/>
+<wire x1="5.3" y1="6.3" x2="13.8" y2="6.3" width="0.127" layer="51"/>
+<wire x1="5.3" y1="-6.3" x2="13.8" y2="-6.3" width="0.127" layer="51"/>
+<pad name="1" x="-1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<wire x1="-2.54" y1="5.715" x2="-2.54" y2="4.445" width="0.2032" layer="22"/>
+<text x="2.54" y="6.477" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="2.54" y="-7.112" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+<package name="2X5-RAF">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Right Angle Female Header&lt;/h3&gt;
+Silk outline of pins
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.54" y1="5.715" x2="-2.54" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="2.7" y1="6.3" x2="11.2" y2="6.3" width="0.2032" layer="21"/>
+<wire x1="11.2" y1="6.3" x2="11.2" y2="-6.3" width="0.2032" layer="21"/>
+<wire x1="11.2" y1="-6.3" x2="2.7" y2="-6.3" width="0.2032" layer="21"/>
+<wire x1="2.7" y1="-6.3" x2="2.7" y2="6.3" width="0.2032" layer="21"/>
+<wire x1="8.2" y1="7" x2="8.2" y2="-6.9" width="0.127" layer="51"/>
+<pad name="1" x="-1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<text x="3.175" y="6.477" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="3.175" y="-7.112" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.54" y1="5.715" x2="-2.54" y2="4.445" width="0.2032" layer="22"/>
+</package>
+<package name="2X5-SHROUDED">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="10.1" x2="4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-10.1" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-2.2" x2="-4.5" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="10.1" x2="4.4" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="-10.1" x2="-4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<pad name="1" x="-1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.27" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.27" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.27" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.27" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.27" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<text x="-4.318" y="10.414" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.318" y="-11.049" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+</package>
+<package name="2X5-SHROUDED_LOCK">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header Locking Footprint&lt;/h3&gt;
+Holes are offset 0.005" from center, to hold pins in place during soldering. 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="10.1" x2="4.5" y2="-10.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="-10.1" x2="-4.5" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="-2.2" x2="-4.627" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="10.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="10.1" x2="4.4" y2="10.1" width="0.2032" layer="51"/>
+<wire x1="4.5" y1="-10.1" x2="-4.5" y2="-10.1" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.627" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<pad name="1" x="-1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<text x="-4.191" y="10.541" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.318" y="-11.049" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+<wire x1="-4.445" y1="10.16" x2="-4.445" y2="8.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="10.16" x2="-3.175" y2="10.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="10.16" x2="4.445" y2="10.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="10.16" x2="4.445" y2="8.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-8.89" x2="-4.445" y2="-10.16" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-10.16" x2="-3.175" y2="-10.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="-10.16" x2="4.445" y2="-10.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="-10.16" x2="4.445" y2="-8.89" width="0.127" layer="21"/>
+</package>
+<package name="2X5-SHROUDED_SMD">
+<description>&lt;h3&gt;Surface Mount - 2x5 Shrouded Header&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-5.315" y1="5.715" x2="-5.315" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="10.1" x2="4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-10.1" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-2.2" x2="-4.5" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="10.1" x2="4.4" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="-10.1" x2="-4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<smd name="1" x="-2.794" y="5.08" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="2" x="2.794" y="5.08" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="3" x="-2.794" y="2.54" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="4" x="2.794" y="2.54" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="5" x="-2.794" y="0" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="6" x="2.794" y="0" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="7" x="-2.794" y="-2.54" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="8" x="2.794" y="-2.54" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="9" x="-2.794" y="-5.08" dx="4.15" dy="1" layer="1" roundness="50"/>
+<smd name="10" x="2.794" y="-5.08" dx="4.15" dy="1" layer="1" roundness="50"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<text x="-4.445" y="10.287" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.445" y="-10.922" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-5.353" y1="5.715" x2="-5.353" y2="4.445" width="0.2032" layer="22"/>
+</package>
+<package name="2X5_NOSILK">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 No Silk Outline&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<pad name="1" x="0" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="2" x="0" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="3" x="2.54" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="4" x="2.54" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="5" x="5.08" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="6" x="5.08" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="7" x="7.62" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="8" x="7.62" y="2.54" drill="1.016" diameter="1.8796"/>
+<pad name="9" x="10.16" y="0" drill="1.016" diameter="1.8796"/>
+<pad name="10" x="10.16" y="2.54" drill="1.016" diameter="1.8796"/>
+<rectangle x1="-0.254" y1="-0.254" x2="0.254" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="-0.254" y1="2.286" x2="0.254" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="2.286" y1="2.286" x2="2.794" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="2.286" y1="-0.254" x2="2.794" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="4.826" y1="-0.254" x2="5.334" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="9.906" y1="2.286" x2="10.414" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="9.906" y1="-0.254" x2="10.414" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="7.366" y1="-0.254" x2="7.874" y2="0.254" layer="51" rot="R90"/>
+<rectangle x1="7.366" y1="2.286" x2="7.874" y2="2.794" layer="51" rot="R90"/>
+<rectangle x1="4.826" y1="2.286" x2="5.334" y2="2.794" layer="51" rot="R90"/>
+<wire x1="0.635" y1="-1.27" x2="-0.635" y2="-1.27" width="0.2032" layer="51"/>
+<text x="-0.889" y="3.81" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-0.762" y="-2.159" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+<package name="2X5_PTH_SILK_.05">
+<description>&lt;h3&gt;Plated Through Hole - 2x5&lt;/h3&gt;
+Holes are 0.05". 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<pad name="1" x="0" y="0" drill="0.4318" rot="R90"/>
+<pad name="2" x="0" y="1.27" drill="0.4318" rot="R90"/>
+<pad name="3" x="1.27" y="0" drill="0.4318" rot="R90"/>
+<pad name="4" x="1.27" y="1.27" drill="0.4318" rot="R90"/>
+<pad name="5" x="2.54" y="0" drill="0.4318" rot="R90"/>
+<pad name="6" x="2.54" y="1.27" drill="0.4318" rot="R90"/>
+<pad name="7" x="3.81" y="0" drill="0.4318" rot="R90"/>
+<pad name="8" x="3.81" y="1.27" drill="0.4318" rot="R90"/>
+<pad name="9" x="5.08" y="0" drill="0.4318" rot="R90"/>
+<pad name="10" x="5.08" y="1.27" drill="0.4318" rot="R90"/>
+<wire x1="-0.635" y1="0.635" x2="-0.762" y2="0.762" width="0.127" layer="21"/>
+<wire x1="-0.762" y1="0.762" x2="-0.762" y2="1.778" width="0.127" layer="21"/>
+<wire x1="-0.762" y1="1.778" x2="-0.508" y2="2.032" width="0.127" layer="21"/>
+<wire x1="-0.508" y1="2.032" x2="0.508" y2="2.032" width="0.127" layer="21"/>
+<wire x1="0.508" y1="2.032" x2="0.635" y2="1.905" width="0.127" layer="21"/>
+<wire x1="0.635" y1="1.905" x2="0.762" y2="2.032" width="0.127" layer="21"/>
+<wire x1="0.762" y1="2.032" x2="1.778" y2="2.032" width="0.127" layer="21"/>
+<wire x1="1.778" y1="2.032" x2="1.905" y2="1.905" width="0.127" layer="21"/>
+<wire x1="1.905" y1="1.905" x2="2.032" y2="2.032" width="0.127" layer="21"/>
+<wire x1="2.032" y1="2.032" x2="3.048" y2="2.032" width="0.127" layer="21"/>
+<wire x1="3.048" y1="2.032" x2="3.175" y2="1.905" width="0.127" layer="21"/>
+<wire x1="3.175" y1="1.905" x2="3.302" y2="2.032" width="0.127" layer="21"/>
+<wire x1="3.302" y1="2.032" x2="4.318" y2="2.032" width="0.127" layer="21"/>
+<wire x1="4.318" y1="2.032" x2="4.445" y2="1.905" width="0.127" layer="21"/>
+<wire x1="4.445" y1="1.905" x2="4.572" y2="2.032" width="0.127" layer="21"/>
+<wire x1="4.572" y1="2.032" x2="5.588" y2="2.032" width="0.127" layer="21"/>
+<wire x1="5.588" y1="2.032" x2="5.842" y2="1.778" width="0.127" layer="21"/>
+<wire x1="5.842" y1="1.778" x2="5.842" y2="0.762" width="0.127" layer="21"/>
+<wire x1="5.842" y1="0.762" x2="5.715" y2="0.635" width="0.127" layer="21"/>
+<wire x1="5.715" y1="0.635" x2="5.842" y2="0.508" width="0.127" layer="21"/>
+<wire x1="5.842" y1="0.508" x2="5.842" y2="-0.508" width="0.127" layer="21"/>
+<wire x1="5.842" y1="-0.508" x2="5.588" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="5.588" y1="-0.762" x2="4.572" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="4.572" y1="-0.762" x2="4.445" y2="-0.635" width="0.127" layer="21"/>
+<wire x1="4.445" y1="-0.635" x2="4.318" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="4.318" y1="-0.762" x2="3.302" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="3.302" y1="-0.762" x2="3.175" y2="-0.635" width="0.127" layer="21"/>
+<wire x1="3.175" y1="-0.635" x2="3.048" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="3.048" y1="-0.762" x2="2.032" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="2.032" y1="-0.762" x2="1.905" y2="-0.635" width="0.127" layer="21"/>
+<wire x1="1.905" y1="-0.635" x2="1.778" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="1.778" y1="-0.762" x2="0.762" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="0.762" y1="-0.762" x2="0.635" y2="-0.635" width="0.127" layer="21"/>
+<wire x1="0.635" y1="-0.635" x2="0.508" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="0.508" y1="-0.762" x2="-0.508" y2="-0.762" width="0.127" layer="21"/>
+<wire x1="-0.508" y1="-0.762" x2="-0.762" y2="-0.508" width="0.127" layer="21"/>
+<wire x1="-0.762" y1="-0.508" x2="-0.762" y2="0.508" width="0.127" layer="21"/>
+<wire x1="-0.762" y1="0.508" x2="-0.635" y2="0.635" width="0.127" layer="21"/>
+<wire x1="0.508" y1="-1.016" x2="-0.508" y2="-1.016" width="0.127" layer="21"/>
+<wire x1="-0.508" y1="-1.016" x2="0.508" y2="-1.016" width="0.127" layer="22"/>
+<text x="-0.762" y="2.286" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-0.762" y="-1.778" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+</package>
+<package name="2X5-SHROUDED-NS">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header No Silk&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="10.1" x2="4.5" y2="-10.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="-10.1" x2="-4.5" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="-2.2" x2="-4.5" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="10.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="10.1" x2="4.4" y2="10.1" width="0.2032" layer="51"/>
+<wire x1="4.5" y1="-10.1" x2="-4.5" y2="-10.1" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-3" y1="-2.2" x2="-4.5" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<pad name="1" x="-1.27" y="5.08" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="2" x="1.27" y="5.08" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="3" x="-1.27" y="2.54" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="4" x="1.27" y="2.54" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="5" x="-1.27" y="0" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="6" x="1.27" y="0" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="7" x="-1.27" y="-2.54" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="8" x="1.27" y="-2.54" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="9" x="-1.27" y="-5.08" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<pad name="10" x="1.27" y="-5.08" drill="1.016" diameter="1.8796" shape="octagon" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<text x="-4.445" y="10.287" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.445" y="-10.922" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+</package>
+<package name="2X5-SHROUDED_LOCK_LATCH">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header Locking Footprint&lt;/h3&gt;
+Holes are offset 0.005" from center, to hold pins in place during soldering. 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="16.1" x2="4.5" y2="-16.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="-16.1" x2="-4.5" y2="-2.2" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="-2.2" x2="-4.627" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="16.1" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="16.1" x2="4.4" y2="16.1" width="0.2032" layer="51"/>
+<wire x1="4.5" y1="-16.1" x2="-4.5" y2="-16.1" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.627" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.627" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<pad name="1" x="-1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<text x="-4.191" y="10.541" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.318" y="-11.049" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+<wire x1="-4.445" y1="16.16" x2="-4.445" y2="14.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="16.16" x2="-3.175" y2="16.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="16.16" x2="4.445" y2="16.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="16.16" x2="4.445" y2="14.89" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-14.89" x2="-4.445" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="-4.445" y1="-16.16" x2="-3.175" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="3.175" y1="-16.16" x2="4.445" y2="-16.16" width="0.127" layer="21"/>
+<wire x1="4.445" y1="-16.16" x2="4.445" y2="-14.89" width="0.127" layer="21"/>
+</package>
+<package name="2X5-SHROUDED_SMD_LONGPADS">
+<description>&lt;h3&gt;Surface Mount - 2x5 Shrouded Header&lt;/h3&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-5.315" y1="5.715" x2="-5.315" y2="4.445" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="10.1" x2="4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-10.1" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="-2.2" x2="-4.5" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="2.2" x2="-4.5" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="-4.5" y1="10.1" x2="4.4" y2="10.1" width="0.2032" layer="21"/>
+<wire x1="4.5" y1="-10.1" x2="-4.5" y2="-10.1" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="3.4" y2="9" width="0.2032" layer="51"/>
+<wire x1="3.4" y1="9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="3.4" y2="-9" width="0.2032" layer="51"/>
+<wire x1="-4.5" y1="2.2" x2="-3" y2="2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="2.2" x2="-3" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3" y1="-2.2" x2="-4.5" y2="-2.2" width="0.2032" layer="21"/>
+<wire x1="-3.4" y1="9" x2="-3.4" y2="2.2" width="0.2032" layer="51"/>
+<wire x1="-3.4" y1="-9" x2="-3.4" y2="-2.2" width="0.2032" layer="51"/>
+<smd name="1" x="-3.294" y="5.08" dx="5.15" dy="1" layer="1" roundness="50"/>
+<smd name="2" x="3.294" y="5.08" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="3" x="-3.294" y="2.54" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="4" x="3.294" y="2.54" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="5" x="-3.294" y="0" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="6" x="3.294" y="0" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="7" x="-3.294" y="-2.54" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="8" x="3.294" y="-2.54" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="9" x="-3.294" y="-5.08" dx="5" dy="1" layer="1" roundness="50"/>
+<smd name="10" x="3.294" y="-5.08" dx="5" dy="1" layer="1" roundness="50"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51"/>
+<text x="-4.445" y="10.287" size="0.6096" layer="25" font="vector" ratio="20">&gt;NAME</text>
+<text x="-4.445" y="-10.922" size="0.6096" layer="27" font="vector" ratio="20">&gt;VALUE</text>
+<wire x1="-5.353" y1="5.715" x2="-5.353" y2="4.445" width="0.2032" layer="22"/>
+</package>
+<package name="2X5-NOSILK_LOCK">
+<description>&lt;h3&gt;Plated Through Hole - 2x5 Shrouded Header Locking Footprint&lt;/h3&gt;
+Holes are offset 0.005" from center, to hold pins in place during soldering. 
+&lt;p&gt;Specifications:
+&lt;ul&gt;&lt;li&gt;Pin count:10&lt;/li&gt;
+&lt;li&gt;Pin pitch:0.1"&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;
+&lt;p&gt;&lt;a href=”https://www.sparkfun.com/datasheets/Prototyping/Shrouded-10pin.pdf”&gt;Datasheet referenced for footprint&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Example device(s):
+&lt;ul&gt;&lt;li&gt;CONN_05x2&lt;/li&gt;
+&lt;/ul&gt;&lt;/p&gt;</description>
+<wire x1="-2.775" y1="5.715" x2="-2.775" y2="4.445" width="0.2032" layer="21"/>
+<pad name="1" x="-1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="2" x="1.397" y="5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="3" x="-1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="4" x="1.397" y="2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="5" x="-1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="6" x="1.397" y="0" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="7" x="-1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="8" x="1.397" y="-2.54" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="9" x="-1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<pad name="10" x="1.397" y="-5.08" drill="1.016" diameter="1.8796" rot="R270"/>
+<rectangle x1="-1.524" y1="4.826" x2="-1.016" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="4.826" x2="1.524" y2="5.334" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="2.286" x2="1.524" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="2.286" x2="-1.016" y2="2.794" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-0.254" x2="1.524" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-0.254" x2="-1.016" y2="0.254" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-5.334" x2="1.524" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-5.334" x2="-1.016" y2="-4.826" layer="51" rot="R270"/>
+<rectangle x1="-1.524" y1="-2.794" x2="-1.016" y2="-2.286" layer="51" rot="R270"/>
+<rectangle x1="1.016" y1="-2.794" x2="1.524" y2="-2.286" layer="51" rot="R270"/>
+<text x="0" y="7.62" size="0.6096" layer="25" font="vector" ratio="20" align="center">&gt;NAME</text>
+<text x="0" y="-7.62" size="0.6096" layer="27" font="vector" ratio="20" align="center">&gt;VALUE</text>
+<wire x1="-2.813" y1="5.715" x2="-2.813" y2="4.445" width="0.2032" layer="22"/>
+</package>
+</packages>
+<symbols>
+<symbol name="CONN_05X2">
+<description>&lt;h3&gt;10 Pin Connection&lt;/h3&gt;
+5x2 pin layout</description>
+<wire x1="3.81" y1="-7.62" x2="-3.81" y2="-7.62" width="0.4064" layer="94"/>
+<wire x1="1.27" y1="0" x2="2.54" y2="0" width="0.6096" layer="94"/>
+<wire x1="1.27" y1="-2.54" x2="2.54" y2="-2.54" width="0.6096" layer="94"/>
+<wire x1="1.27" y1="-5.08" x2="2.54" y2="-5.08" width="0.6096" layer="94"/>
+<wire x1="-3.81" y1="7.62" x2="-3.81" y2="-7.62" width="0.4064" layer="94"/>
+<wire x1="3.81" y1="-7.62" x2="3.81" y2="7.62" width="0.4064" layer="94"/>
+<wire x1="-3.81" y1="7.62" x2="3.81" y2="7.62" width="0.4064" layer="94"/>
+<wire x1="1.27" y1="5.08" x2="2.54" y2="5.08" width="0.6096" layer="94"/>
+<wire x1="1.27" y1="2.54" x2="2.54" y2="2.54" width="0.6096" layer="94"/>
+<wire x1="-1.27" y1="0" x2="-2.54" y2="0" width="0.6096" layer="94"/>
+<wire x1="-1.27" y1="-2.54" x2="-2.54" y2="-2.54" width="0.6096" layer="94"/>
+<wire x1="-1.27" y1="-5.08" x2="-2.54" y2="-5.08" width="0.6096" layer="94"/>
+<wire x1="-1.27" y1="5.08" x2="-2.54" y2="5.08" width="0.6096" layer="94"/>
+<wire x1="-1.27" y1="2.54" x2="-2.54" y2="2.54" width="0.6096" layer="94"/>
+<text x="-3.81" y="-9.906" size="1.778" layer="96" font="vector">&gt;VALUE</text>
+<text x="-3.81" y="8.128" size="1.778" layer="95" font="vector">&gt;NAME</text>
+<pin name="10" x="7.62" y="-5.08" visible="pad" length="middle" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="8" x="7.62" y="-2.54" visible="pad" length="middle" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="6" x="7.62" y="0" visible="pad" length="middle" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="4" x="7.62" y="2.54" visible="pad" length="middle" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="2" x="7.62" y="5.08" visible="pad" length="middle" direction="pas" swaplevel="1" rot="R180"/>
+<pin name="9" x="-7.62" y="-5.08" visible="pad" length="middle" direction="pas" swaplevel="1"/>
+<pin name="7" x="-7.62" y="-2.54" visible="pad" length="middle" direction="pas" swaplevel="1"/>
+<pin name="5" x="-7.62" y="0" visible="pad" length="middle" direction="pas" swaplevel="1"/>
+<pin name="3" x="-7.62" y="2.54" visible="pad" length="middle" direction="pas" swaplevel="1"/>
+<pin name="1" x="-7.62" y="5.08" visible="pad" length="middle" direction="pas" swaplevel="1"/>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="CONN_05X2" prefix="J" uservalue="yes">
+<description>&lt;h3&gt;Multi connection point. Often used as Generic Header-pin footprint for 0.1 inch spaced/style header connections&lt;/h3&gt;
+
+&lt;p&gt;&lt;/p&gt;
+&lt;h3&gt;For AVR SPI programming port, see special device with nice symbol: "AVR_SPI_PROG_5x2.dev"&lt;/h3&gt;
+
+&lt;p&gt;&lt;/p&gt;
+&lt;b&gt;You can populate with any combo of single row headers, but if you'd like an exact match, check these:&lt;/b&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/778"&gt; 2x5 AVR ICSP Male Header&lt;/a&gt; (PRT-00778)&lt;/li&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/8506"&gt; 2x5 Pin Shrouded Header&lt;/a&gt; (PRT-08506)&lt;/li&gt;
+&lt;/ul&gt;
+
+&lt;p&gt;&lt;/p&gt;
+&lt;b&gt;On any of the 0.1 inch spaced packages, you can populate with these:&lt;/b&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/116"&gt; Break Away Headers - Straight&lt;/a&gt; (PRT-00116)&lt;/li&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/553"&gt; Break Away Male Headers - Right Angle&lt;/a&gt; (PRT-00553)&lt;/li&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/115"&gt; Female Headers&lt;/a&gt; (PRT-00115)&lt;/li&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/117"&gt; Break Away Headers - Machine Pin&lt;/a&gt; (PRT-00117)&lt;/li&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/743"&gt; Break Away Female Headers - Swiss Machine Pin&lt;/a&gt; (PRT-00743)&lt;/li&gt;
+&lt;/ul&gt;
+
+&lt;p&gt;&lt;/p&gt;
+&lt;b&gt;Special note: the shrouded connector mates well with our 5x2 ribbon cables:&lt;/b&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;a href="https://www.sparkfun.com/products/8535"&gt; 2x5 Pin IDC Ribbon Cable&lt;/a&gt; (PRT-08535)&lt;/li&gt;
+&lt;/ul&gt;</description>
+<gates>
+<gate name="G$1" symbol="CONN_05X2" x="0" y="0"/>
+</gates>
+<devices>
+<device name="PTH" package="2X5">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PROD_ID" value="CONN-08499" constant="no"/>
+<attribute name="SF_ID" value="PRT-0778" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="RA" package="2X5-RA">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="RAF" package="2X5-RAF">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="SHD" package="2X5-SHROUDED">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PROD_ID" value="CONN-08671" constant="no"/>
+<attribute name="SF_ID" value="PRT-08506" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="SHD_LOCK" package="2X5-SHROUDED_LOCK">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PROD_ID" value="CONN-08671" constant="no"/>
+<attribute name="SF_ID" value="PRT-08506" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="SHD_SMD" package="2X5-SHROUDED_SMD">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PROD_ID" value="CONN-09508" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="NO_SILK" package="2X5_NOSILK">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0.05_IN_PTH_SILK" package="2X5_PTH_SILK_.05">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="SHD-NS" package="2X5-SHROUDED-NS">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name="">
+<attribute name="PROD_ID" value="CONN-08671" constant="no"/>
+<attribute name="SF_ID" value="PRT-08506" constant="no"/>
+</technology>
+</technologies>
+</device>
+<device name="SHD_LOCK_LATCH" package="2X5-SHROUDED_LOCK_LATCH">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="SMD_LONGPADS" package="2X5-SHROUDED_SMD_LONGPADS">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="NO_SILK_LOCK" package="2X5-NOSILK_LOCK">
+<connects>
+<connect gate="G$1" pin="1" pad="1"/>
+<connect gate="G$1" pin="10" pad="10"/>
+<connect gate="G$1" pin="2" pad="2"/>
+<connect gate="G$1" pin="3" pad="3"/>
+<connect gate="G$1" pin="4" pad="4"/>
+<connect gate="G$1" pin="5" pad="5"/>
+<connect gate="G$1" pin="6" pad="6"/>
+<connect gate="G$1" pin="7" pad="7"/>
+<connect gate="G$1" pin="8" pad="8"/>
+<connect gate="G$1" pin="9" pad="9"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="lights">
+<packages>
+<package name="LED1206">
+<description>LED 1206 pads (standard pattern)</description>
+<wire x1="0.9525" y1="-0.8128" x2="-0.9652" y2="-0.8128" width="0.1524" layer="51"/>
+<wire x1="0.9525" y1="0.8128" x2="-0.9652" y2="0.8128" width="0.1524" layer="51"/>
+<smd name="2" x="1.422" y="0" dx="1.6" dy="1.803" layer="1"/>
+<smd name="1" x="-1.422" y="0" dx="1.6" dy="1.803" layer="1"/>
+<text x="-1.27" y="1.27" size="1.27" layer="25">&gt;NAME</text>
+<text x="-1.27" y="-2.54" size="1.27" layer="27">&gt;VALUE</text>
+<rectangle x1="-1.6891" y1="-0.8763" x2="-0.9525" y2="0.8763" layer="51"/>
+<rectangle x1="0.9525" y1="-0.8763" x2="1.6891" y2="0.8763" layer="51"/>
+<wire x1="2.5" y1="0.8" x2="2.5" y2="-0.8" width="0.127" layer="21"/>
+<wire x1="-0.3" y1="0.5" x2="-0.3" y2="-0.5" width="0.127" layer="21"/>
+<wire x1="-0.3" y1="-0.5" x2="0.3" y2="0" width="0.127" layer="21"/>
+<wire x1="0.3" y1="0" x2="-0.3" y2="0.5" width="0.127" layer="21"/>
+</package>
+<package name="LED1206FAB">
+<description>LED1206 FAB style (smaller pads to allow trace between)</description>
+<wire x1="-2.032" y1="1.016" x2="2.032" y2="1.016" width="0.127" layer="21"/>
+<wire x1="2.032" y1="1.016" x2="2.032" y2="-1.016" width="0.127" layer="21"/>
+<wire x1="2.032" y1="-1.016" x2="-2.032" y2="-1.016" width="0.127" layer="21"/>
+<wire x1="-2.032" y1="-1.016" x2="-2.032" y2="1.016" width="0.127" layer="21"/>
+<smd name="1" x="-1.651" y="0" dx="1.27" dy="1.905" layer="1"/>
+<smd name="2" x="1.651" y="0" dx="1.27" dy="1.905" layer="1"/>
+<text x="-1.778" y="1.27" size="1.016" layer="25" ratio="15">&gt;NAME</text>
+<text x="-1.778" y="-2.286" size="1.016" layer="27" ratio="15">&gt;VALUE</text>
+</package>
+<package name="5MM">
+<description>5mm round through hole part.</description>
+<wire x1="2.54" y1="-1.905" x2="2.54" y2="1.905" width="0.2032" layer="21"/>
+<wire x1="2.54" y1="-1.905" x2="2.54" y2="1.905" width="0.254" layer="21" curve="-286.260205" cap="flat"/>
+<wire x1="-1.143" y1="0" x2="0" y2="1.143" width="0.1524" layer="51" curve="-90" cap="flat"/>
+<wire x1="0" y1="-1.143" x2="1.143" y2="0" width="0.1524" layer="51" curve="90" cap="flat"/>
+<wire x1="-1.651" y1="0" x2="0" y2="1.651" width="0.1524" layer="51" curve="-90" cap="flat"/>
+<wire x1="0" y1="-1.651" x2="1.651" y2="0" width="0.1524" layer="51" curve="90" cap="flat"/>
+<wire x1="-2.159" y1="0" x2="0" y2="2.159" width="0.1524" layer="51" curve="-90" cap="flat"/>
+<wire x1="0" y1="-2.159" x2="2.159" y2="0" width="0.1524" layer="51" curve="90" cap="flat"/>
+<circle x="0" y="0" radius="2.54" width="0.1524" layer="21"/>
+<pad name="IN" x="-1.27" y="0" drill="0.8128" diameter="1.4224"/>
+<pad name="OUT" x="1.27" y="0" drill="0.8128" diameter="1.4224"/>
+<text x="3.175" y="0.5334" size="1.27" layer="25" ratio="10">&gt;NAME</text>
+<text x="3.2004" y="-1.8034" size="1.27" layer="27" ratio="10">&gt;VALUE</text>
+</package>
+<package name="LED0805">
+<smd name="1" x="-0.85" y="0" dx="1.1" dy="1" layer="1"/>
+<smd name="2" x="0.85" y="0" dx="1.1" dy="1" layer="1"/>
+<text x="-0.889" y="1.397" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.016" y="-2.413" size="1.016" layer="27" ratio="10">&gt;VALUE</text>
+<rectangle x1="-0.1999" y1="-0.3" x2="0.1999" y2="0.3" layer="35"/>
+<wire x1="-0.0778" y1="0.2818" x2="0.1278" y2="0" width="0.127" layer="21"/>
+<wire x1="0.1278" y1="0" x2="-0.0778" y2="-0.2818" width="0.127" layer="21"/>
+<wire x1="-0.0778" y1="0.2818" x2="-0.0778" y2="-0.2818" width="0.127" layer="21"/>
+</package>
+<package name="0402-D">
+<description>&lt;b&gt;CAPACITOR&lt;/b&gt;&lt;p&gt;
+chip</description>
+<wire x1="-0.245" y1="0.224" x2="0.245" y2="0.224" width="0.1524" layer="51"/>
+<wire x1="0.245" y1="-0.224" x2="-0.245" y2="-0.224" width="0.1524" layer="51"/>
+<smd name="1" x="-0.525" y="0" dx="0.575" dy="0.7" layer="1"/>
+<smd name="2" x="0.525" y="0" dx="0.575" dy="0.7" layer="1"/>
+<text x="-0.889" y="0.6985" size="1.016" layer="25">&gt;NAME</text>
+<text x="-1.0795" y="-1.778" size="1.016" layer="27">&gt;VALUE</text>
+<rectangle x1="-0.554" y1="-0.3048" x2="-0.254" y2="0.2951" layer="51"/>
+<rectangle x1="0.2588" y1="-0.3048" x2="0.5588" y2="0.2951" layer="51"/>
+<rectangle x1="-0.1999" y1="-0.3" x2="0.1999" y2="0.3" layer="35"/>
+<wire x1="-1" y1="-0.2" x2="-1" y2="0.2" width="0.127" layer="21"/>
+</package>
+<package name="LED0603-RIGHTANGLE">
+<smd name="1" x="-1" y="0" dx="1.1" dy="1.1" layer="1"/>
+<smd name="2" x="1" y="0" dx="1.1" dy="1.1" layer="1"/>
+<text x="-0.089" y="1.197" size="0.8128" layer="25" align="center">&gt;NAME</text>
+<text x="-0.016" y="-1.313" size="0.8128" layer="27" ratio="10" align="center">&gt;VALUE</text>
+<rectangle x1="-0.1999" y1="-0.3" x2="0.1999" y2="0.3" layer="35"/>
+<wire x1="-0.1778" y1="0.3818" x2="0.2278" y2="0" width="0.127" layer="21"/>
+<wire x1="0.2278" y1="0" x2="-0.1778" y2="-0.3818" width="0.127" layer="21"/>
+<wire x1="-0.1778" y1="0.3818" x2="-0.1778" y2="-0.3818" width="0.127" layer="21"/>
+<wire x1="-1" y1="-0.2" x2="1" y2="-0.2" width="0.127" layer="51"/>
+<wire x1="1" y1="-0.2" x2="0.4" y2="-0.8" width="0.127" layer="51" curve="-90"/>
+<wire x1="0.4" y1="-0.8" x2="-0.4" y2="-0.8" width="0.127" layer="51"/>
+<wire x1="-0.4" y1="-0.8" x2="-1" y2="-0.2" width="0.127" layer="51" curve="-90"/>
+</package>
+<package name="LED-5630">
+<smd name="P$2" x="2.6" y="0.6" dx="0.6" dy="0.8" layer="1" thermals="no"/>
+<smd name="P$3" x="2.6" y="-0.6" dx="0.6" dy="0.8" layer="1" thermals="no"/>
+<smd name="P$4" x="-2.6" y="0.6" dx="0.6" dy="0.8" layer="1" thermals="no"/>
+<smd name="P$5" x="-2.6" y="-0.6" dx="0.6" dy="0.8" layer="1" thermals="no"/>
+<rectangle x1="-2.5" y1="-1.5" x2="2.5" y2="1.5" layer="51"/>
+<wire x1="0" y1="1.4" x2="0" y2="1.9" width="0.1016" layer="21"/>
+<wire x1="0" y1="-1.4" x2="0" y2="-1.9" width="0.1016" layer="21"/>
+<polygon width="0.1524" layer="1" pour="solid">
+<vertex x="-2.21640625" y="-1"/>
+<vertex x="-2.1884" y="-1.0116"/>
+<vertex x="-2" y="-1.2"/>
+<vertex x="-1.4" y="-1.2"/>
+<vertex x="-1.4" y="1.2"/>
+<vertex x="-2" y="1.2"/>
+<vertex x="-2.1884" y="1.0116"/>
+<vertex x="-2.21640625" y="1"/>
+<vertex x="-2.4" y="1"/>
+<vertex x="-2.4" y="-1"/>
+</polygon>
+<polygon width="0.1524" layer="1" pour="solid">
+<vertex x="2.4" y="-1"/>
+<vertex x="2.4" y="1"/>
+<vertex x="2.21640625" y="1"/>
+<vertex x="2.1884" y="1.0116"/>
+<vertex x="2" y="1.2"/>
+<vertex x="-0.8" y="1.2"/>
+<vertex x="-0.8" y="-1.2"/>
+<vertex x="2" y="-1.2"/>
+<vertex x="2.1884" y="-1.0116"/>
+<vertex x="2.21640625" y="-1"/>
+</polygon>
+<polygon width="0.1524" layer="29" pour="solid">
+<vertex x="3" y="0.1"/>
+<vertex x="3" y="1.1"/>
+<vertex x="2.21640625" y="1.1"/>
+<vertex x="2.1884" y="1.1116"/>
+<vertex x="2" y="1.3"/>
+<vertex x="-0.9" y="1.3"/>
+<vertex x="-0.9" y="-1.3"/>
+<vertex x="2" y="-1.3"/>
+<vertex x="2.1884" y="-1.1116"/>
+<vertex x="2.21640625" y="-1.1"/>
+<vertex x="3" y="-1.1"/>
+<vertex x="3" y="-0.1"/>
+<vertex x="2.46104375" y="-0.1"/>
+<vertex x="2.4330375" y="-0.0884"/>
+<vertex x="2.4116" y="-0.0669625"/>
+<vertex x="2.4" y="-0.03895625"/>
+<vertex x="2.4" y="0.03895625"/>
+<vertex x="2.4116" y="0.0669625"/>
+<vertex x="2.4330375" y="0.0884"/>
+<vertex x="2.46104375" y="0.1"/>
+</polygon>
+<polygon width="0.1524" layer="29" pour="solid">
+<vertex x="-2.21640625" y="-1.1"/>
+<vertex x="-2.1884" y="-1.1116"/>
+<vertex x="-2" y="-1.3"/>
+<vertex x="-1.3" y="-1.3"/>
+<vertex x="-1.3" y="1.3"/>
+<vertex x="-2" y="1.3"/>
+<vertex x="-2.1884" y="1.1116"/>
+<vertex x="-2.21640625" y="1.1"/>
+<vertex x="-3" y="1.1"/>
+<vertex x="-3" y="0.1"/>
+<vertex x="-2.56104375" y="0.1"/>
+<vertex x="-2.5330375" y="0.0884"/>
+<vertex x="-2.5116" y="0.0669625"/>
+<vertex x="-2.5" y="0.03895625"/>
+<vertex x="-2.5" y="-0.03895625"/>
+<vertex x="-2.5116" y="-0.0669625"/>
+<vertex x="-2.5330375" y="-0.0884"/>
+<vertex x="-2.56104375" y="-0.1"/>
+<vertex x="-3" y="-0.1"/>
+<vertex x="-3" y="-1.1"/>
+</polygon>
+<polygon width="0.1524" layer="31" pour="solid">
+<vertex x="-2" y="-1.1"/>
+<vertex x="-1.5" y="-1.1"/>
+<vertex x="-1.5" y="1.1"/>
+<vertex x="-2" y="1.1"/>
+</polygon>
+<polygon width="0.1524" layer="31" pour="solid">
+<vertex x="-0.7" y="-1.1"/>
+<vertex x="2" y="-1.1"/>
+<vertex x="2" y="1.1"/>
+<vertex x="-0.7" y="1.1"/>
+</polygon>
+<wire x1="2.6" y1="-1.2" x2="2.8" y2="-1.4" width="0.1016" layer="21"/>
+<wire x1="2.8" y1="-1.4" x2="2.6" y2="-1.6" width="0.1016" layer="21"/>
+<wire x1="2.6" y1="-1.6" x2="2.6" y2="-1.2" width="0.1016" layer="21"/>
+<wire x1="2.9" y1="-1.2" x2="2.9" y2="-1.6" width="0.1016" layer="21"/>
+</package>
+</packages>
+<symbols>
+<symbol name="LED">
+<description>LED</description>
+<wire x1="1.27" y1="2.54" x2="0" y2="0" width="0.254" layer="94"/>
+<wire x1="0" y1="0" x2="-1.27" y2="2.54" width="0.254" layer="94"/>
+<wire x1="1.27" y1="0" x2="0" y2="0" width="0.254" layer="94"/>
+<wire x1="0" y1="0" x2="-1.27" y2="0" width="0.254" layer="94"/>
+<wire x1="1.27" y1="2.54" x2="0" y2="2.54" width="0.254" layer="94"/>
+<wire x1="0" y1="2.54" x2="-1.27" y2="2.54" width="0.254" layer="94"/>
+<wire x1="0" y1="2.54" x2="0" y2="0" width="0.1524" layer="94"/>
+<wire x1="-2.032" y1="1.778" x2="-3.429" y2="0.381" width="0.1524" layer="94"/>
+<wire x1="-1.905" y1="0.635" x2="-3.302" y2="-0.762" width="0.1524" layer="94"/>
+<text x="3.556" y="-2.032" size="1.778" layer="95" rot="R90">&gt;NAME</text>
+<text x="5.715" y="-2.032" size="1.778" layer="96" rot="R90">&gt;VALUE</text>
+<pin name="C" x="0" y="-2.54" visible="off" length="short" direction="pas" rot="R90"/>
+<pin name="A" x="0" y="5.08" visible="off" length="short" direction="pas" rot="R270"/>
+<polygon width="0.1524" layer="94" pour="solid">
+<vertex x="-3.048" y="1.27"/>
+<vertex x="-3.429" y="0.381"/>
+<vertex x="-2.54" y="0.762"/>
+</polygon>
+<polygon width="0.1524" layer="94" pour="solid">
+<vertex x="-2.921" y="0.127"/>
+<vertex x="-3.302" y="-0.762"/>
+<vertex x="-2.413" y="-0.381"/>
+</polygon>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="LED" prefix="D">
+<description>LED</description>
+<gates>
+<gate name="G$1" symbol="LED" x="0" y="0"/>
+</gates>
+<devices>
+<device name="1206" package="LED1206">
+<connects>
+<connect gate="G$1" pin="A" pad="1"/>
+<connect gate="G$1" pin="C" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="FAB1206" package="LED1206FAB">
+<connects>
+<connect gate="G$1" pin="A" pad="1"/>
+<connect gate="G$1" pin="C" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="5MM" package="5MM">
+<connects>
+<connect gate="G$1" pin="A" pad="IN"/>
+<connect gate="G$1" pin="C" pad="OUT"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0805" package="LED0805">
+<connects>
+<connect gate="G$1" pin="A" pad="1"/>
+<connect gate="G$1" pin="C" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="5630" package="LED-5630">
+<connects>
+<connect gate="G$1" pin="A" pad="P$4 P$5"/>
+<connect gate="G$1" pin="C" pad="P$2 P$3"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0402" package="0402-D">
+<connects>
+<connect gate="G$1" pin="A" pad="2"/>
+<connect gate="G$1" pin="C" pad="1"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="0603RA" package="LED0603-RIGHTANGLE">
+<connects>
+<connect gate="G$1" pin="A" pad="1"/>
+<connect gate="G$1" pin="C" pad="2"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="sensor">
+<packages>
+<package name="TSSOP14">
+<description>&lt;b&gt;Thin Shrink Small Outline Plastic 14&lt;/b&gt;</description>
+<wire x1="-2.5146" y1="-2.0828" x2="2.5146" y2="-2.0828" width="0.1524" layer="51"/>
+<wire x1="2.5146" y1="2.0828" x2="2.5146" y2="-2.0828" width="0.1524" layer="51"/>
+<wire x1="2.5146" y1="2.0828" x2="-2.5146" y2="2.0828" width="0.1524" layer="51"/>
+<wire x1="-2.5146" y1="-2.0828" x2="-2.5146" y2="2.0828" width="0.1524" layer="51"/>
+<circle x="-3.0956" y="-1.6192" radius="0.3048" width="0.1524" layer="21"/>
+<smd name="1" x="-1.905" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="2" x="-1.27" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="3" x="-0.635" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="4" x="0" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="5" x="0.635" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="6" x="1.27" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="7" x="1.905" y="-2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="14" x="-1.905" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="13" x="-1.27" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="12" x="-0.635" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="11" x="0" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="10" x="0.635" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="9" x="1.27" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<smd name="8" x="1.905" y="2.7178" dx="0.3048" dy="0.9906" layer="1"/>
+<text x="-2.8956" y="-2.0828" size="1.016" layer="25" ratio="10" rot="R90">&gt;NAME</text>
+<text x="3.8862" y="-2.0828" size="1.016" layer="27" ratio="10" rot="R90">&gt;VALUE</text>
+<rectangle x1="1.8034" y1="2.1082" x2="2.0066" y2="2.9464" layer="51"/>
+<rectangle x1="1.1684" y1="2.1082" x2="1.3716" y2="2.9464" layer="51"/>
+<rectangle x1="0.5334" y1="2.1082" x2="0.7366" y2="2.9464" layer="51"/>
+<rectangle x1="-0.1016" y1="2.1082" x2="0.1016" y2="2.9464" layer="51"/>
+<rectangle x1="-0.7366" y1="2.1082" x2="-0.5334" y2="2.9464" layer="51"/>
+<rectangle x1="-1.3716" y1="2.1082" x2="-1.1684" y2="2.9464" layer="51"/>
+<rectangle x1="-2.0066" y1="2.1082" x2="-1.8034" y2="2.9464" layer="51"/>
+<rectangle x1="-2.0066" y1="-2.921" x2="-1.8034" y2="-2.0828" layer="51"/>
+<rectangle x1="-1.3716" y1="-2.921" x2="-1.1684" y2="-2.0828" layer="51"/>
+<rectangle x1="-0.7366" y1="-2.921" x2="-0.5334" y2="-2.0828" layer="51"/>
+<rectangle x1="-0.1016" y1="-2.921" x2="0.1016" y2="-2.0828" layer="51"/>
+<rectangle x1="0.5334" y1="-2.921" x2="0.7366" y2="-2.0828" layer="51"/>
+<rectangle x1="1.1684" y1="-2.921" x2="1.3716" y2="-2.0828" layer="51"/>
+<rectangle x1="1.8034" y1="-2.921" x2="2.0066" y2="-2.0828" layer="51"/>
+</package>
+</packages>
+<symbols>
+<symbol name="AS5047">
+<pin name="MOSI" x="-15.24" y="0" length="middle"/>
+<pin name="MISO" x="-15.24" y="2.54" length="middle"/>
+<pin name="CLK" x="-15.24" y="5.08" length="middle"/>
+<pin name="CSN" x="-15.24" y="7.62" length="middle"/>
+<pin name="TEST" x="-15.24" y="-2.54" length="middle"/>
+<pin name="A" x="-15.24" y="-5.08" length="middle"/>
+<pin name="B" x="-15.24" y="-7.62" length="middle"/>
+<pin name="I" x="15.24" y="7.62" length="middle" rot="R180"/>
+<pin name="GND" x="15.24" y="5.08" length="middle" rot="R180"/>
+<pin name="VDD3V" x="15.24" y="2.54" length="middle" rot="R180"/>
+<pin name="VDD" x="15.24" y="0" length="middle" rot="R180"/>
+<pin name="U" x="15.24" y="-2.54" length="middle" rot="R180"/>
+<pin name="V" x="15.24" y="-5.08" length="middle" rot="R180"/>
+<pin name="W" x="15.24" y="-7.62" length="middle" rot="R180"/>
+<wire x1="-10.16" y1="10.16" x2="10.16" y2="10.16" width="0.254" layer="94"/>
+<wire x1="10.16" y1="10.16" x2="10.16" y2="-10.16" width="0.254" layer="94"/>
+<wire x1="10.16" y1="-10.16" x2="-10.16" y2="-10.16" width="0.254" layer="94"/>
+<wire x1="-10.16" y1="-10.16" x2="-10.16" y2="10.16" width="0.254" layer="94"/>
+<text x="-2.54" y="12.7" size="1.27" layer="95">&gt;NAME</text>
+<text x="-2.54" y="-12.7" size="1.27" layer="96">&gt;VALUE</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="AS5047" prefix="U">
+<gates>
+<gate name="G$1" symbol="AS5047" x="0" y="0"/>
+</gates>
+<devices>
+<device name="" package="TSSOP14">
+<connects>
+<connect gate="G$1" pin="A" pad="6"/>
+<connect gate="G$1" pin="B" pad="7"/>
+<connect gate="G$1" pin="CLK" pad="2"/>
+<connect gate="G$1" pin="CSN" pad="1"/>
+<connect gate="G$1" pin="GND" pad="13"/>
+<connect gate="G$1" pin="I" pad="14"/>
+<connect gate="G$1" pin="MISO" pad="3"/>
+<connect gate="G$1" pin="MOSI" pad="4"/>
+<connect gate="G$1" pin="TEST" pad="5"/>
+<connect gate="G$1" pin="U" pad="10"/>
+<connect gate="G$1" pin="V" pad="9"/>
+<connect gate="G$1" pin="VDD" pad="11"/>
+<connect gate="G$1" pin="VDD3V" pad="12"/>
+<connect gate="G$1" pin="W" pad="8"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+<library name="sensor" urn="urn:adsk.wipprod:fs.file:vf.erO8XLirRvWBRSYvGu7cAQ">
+<packages>
+<package name="TO-92-AMMO" library_version="1">
+<description>&lt;h3&gt;TO-92 3-Pin PTH AMMO package&lt;/h3&gt;
+&lt;p&gt;&lt;a href=""&gt;Datasheet&lt;/a&gt;&lt;/p&gt;
+&lt;p&gt;Specifications:
+&lt;ul&gt;
+&lt;li&gt;Pin Count: 3&lt;/li&gt;
+&lt;li&gt;Dimensions:  3.68 x 4.83 x 4.83 mm&lt;/li&gt;
+&lt;li&gt;Pitch: 2.54 mm&lt;/li&gt;
+&lt;/ul&gt;
+&lt;p&gt;Devices Using:&lt;/p&gt;
+&lt;ul&gt;
+&lt;li&gt;DS18B20&lt;/li&gt;
+&lt;/ul&gt;</description>
+<pad name="3" x="1.27" y="0" drill="0.55"/>
+<pad name="2" x="0" y="0" drill="0.55"/>
+<pad name="1" x="-1.27" y="0" drill="0.55" shape="square"/>
+<text x="0" y="-1.278" size="0.6096" layer="25" font="vector" ratio="20" rot="R180" align="bottom-center">&gt;NAME</text>
+<text x="0" y="1.294" size="0.6096" layer="27" font="vector" ratio="20" rot="R180" align="top-center">&gt;VALUE</text>
+<rectangle x1="-2.05" y1="-1" x2="2.05" y2="0.5" layer="51"/>
+<wire x1="0" y1="0" x2="0" y2="-15" width="0.127" layer="51"/>
+</package>
+<package name="SOT23" library_version="1">
+<description>&lt;b&gt;SOT 23&lt;/b&gt;</description>
+<wire x1="1.4224" y1="0.6604" x2="1.4224" y2="-0.6604" width="0.1524" layer="51"/>
+<wire x1="1.4224" y1="-0.6604" x2="-1.4224" y2="-0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="-0.6604" x2="-1.4224" y2="0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="0.6604" x2="1.4224" y2="0.6604" width="0.1524" layer="51"/>
+<wire x1="-1.4224" y1="-0.1524" x2="-1.4224" y2="0.6604" width="0.1524" layer="21"/>
+<wire x1="-1.4224" y1="0.6604" x2="-0.8636" y2="0.6604" width="0.1524" layer="21"/>
+<wire x1="1.4224" y1="0.6604" x2="1.4224" y2="-0.1524" width="0.1524" layer="21"/>
+<wire x1="0.8636" y1="0.6604" x2="1.4224" y2="0.6604" width="0.1524" layer="21"/>
+<smd name="3" x="0" y="1.1" dx="0.762" dy="1.016" layer="1"/>
+<smd name="2" x="0.95" y="-1.1" dx="0.762" dy="1.016" layer="1"/>
+<smd name="1" x="-0.95" y="-1.1" dx="0.762" dy="1.016" layer="1"/>
+<text x="-1.905" y="1.905" size="1.27" layer="25">&gt;NAME</text>
+<text x="-1.905" y="-3.175" size="1.27" layer="27">&gt;VALUE</text>
+<rectangle x1="-0.2286" y1="0.7112" x2="0.2286" y2="1.2954" layer="51"/>
+<rectangle x1="0.7112" y1="-1.2954" x2="1.1684" y2="-0.7112" layer="51"/>
+<rectangle x1="-1.1684" y1="-1.2954" x2="-0.7112" y2="-0.7112" layer="51"/>
+</package>
+</packages>
+<symbols>
+<symbol name="HALL" library_version="1">
+<pin name="VCC" x="-7.62" y="0" length="middle" rot="R90"/>
+<pin name="GND" x="0" y="0" length="middle" rot="R90"/>
+<pin name="OUT" x="7.62" y="0" length="middle" rot="R90"/>
+<wire x1="-10.16" y1="5.08" x2="-10.16" y2="15.24" width="0.254" layer="94"/>
+<wire x1="-10.16" y1="15.24" x2="10.16" y2="15.24" width="0.254" layer="94"/>
+<wire x1="10.16" y1="15.24" x2="10.16" y2="5.08" width="0.254" layer="94"/>
+<wire x1="10.16" y1="5.08" x2="-10.16" y2="5.08" width="0.254" layer="94"/>
+<text x="-10.16" y="17.78" size="1.27" layer="95">&gt;NAME</text>
+<text x="2.54" y="17.78" size="1.27" layer="96">&gt;VALUE</text>
+</symbol>
+</symbols>
+<devicesets>
+<deviceset name="HALL" prefix="U" library_version="1">
+<gates>
+<gate name="G$1" symbol="HALL" x="0" y="-5.08"/>
+</gates>
+<devices>
+<device name="" package="TO-92-AMMO">
+<connects>
+<connect gate="G$1" pin="GND" pad="2"/>
+<connect gate="G$1" pin="OUT" pad="3"/>
+<connect gate="G$1" pin="VCC" pad="1"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+<device name="SOT23" package="SOT23">
+<connects>
+<connect gate="G$1" pin="GND" pad="3"/>
+<connect gate="G$1" pin="OUT" pad="2"/>
+<connect gate="G$1" pin="VCC" pad="1"/>
+</connects>
+<technologies>
+<technology name=""/>
+</technologies>
+</device>
+</devices>
+</deviceset>
+</devicesets>
+</library>
+</libraries>
+<attributes>
+</attributes>
+<variantdefs>
+</variantdefs>
+<classes>
+<class number="0" name="default" width="0" drill="0">
+</class>
+</classes>
+<parts>
+<part name="U1" library="microcontrollers" deviceset="ATSAMD21E18A-AF" device="FAB"/>
+<part name="J1" library="SparkFun-Connectors" library_urn="urn:adsk.eagle:library:513" deviceset="CORTEX_DEBUG" device="_PTH" package3d_urn="urn:adsk.eagle:package:38290/1"/>
+<part name="P+1" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+5V" device=""/>
+<part name="+3V1" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="GND1" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="GND2" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="GND3" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="+3V2" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="S1" library="passives" deviceset="2-8X4-5_SWITCH" device=""/>
+<part name="C1" library="passives" deviceset="CAP" device="0805" value="0.1uF"/>
+<part name="C2" library="passives" deviceset="CAP" device="0805" value="1uF"/>
+<part name="+3V3" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="GND4" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="+3V4" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="R1" library="passives" deviceset="RESISTOR" device="0805-RES" value="10k"/>
+<part name="U2" library="comm" deviceset="RS485-ISL83078E" device="MSOP"/>
+<part name="U3" library="comm" deviceset="RS485-ISL83078E" device="MSOP"/>
+<part name="X1" library="connector" deviceset="USB" device=""/>
+<part name="+3V5" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="U4" library="power" deviceset="VREG-AP2112" device=""/>
+<part name="C3" library="passives" deviceset="CAP" device="0805" value="10uF"/>
+<part name="C4" library="passives" deviceset="CAP" device="0805" value="10uF"/>
+<part name="P+2" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+5V" device=""/>
+<part name="+3V6" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="GND5" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="J2" library="SparkFun-Connectors" deviceset="CONN_05X2" device="SHD_LOCK_LATCH"/>
+<part name="GND6" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="GND7" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="P+3" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+5V" device=""/>
+<part name="P+4" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+5V" device=""/>
+<part name="GND8" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="GND9" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="D1" library="lights" deviceset="LED" device="0805"/>
+<part name="R2" library="passives" deviceset="RESISTOR" device="0805-RES" value="470R"/>
+<part name="GND10" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="D2" library="lights" deviceset="LED" device="0805"/>
+<part name="R3" library="passives" deviceset="RESISTOR" device="0805-RES" value="470R"/>
+<part name="GND11" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="S2" library="passives" deviceset="2-8X4-5_SWITCH" device=""/>
+<part name="GND12" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="R4" library="passives" deviceset="RESISTOR" device="0805-RES" value="10k"/>
+<part name="+3V7" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="U5" library="sensor" deviceset="AS5047" device=""/>
+<part name="C5" library="passives" deviceset="CAP" device="0805" value="0.1uF"/>
+<part name="U6" library="sensor" library_urn="urn:adsk.wipprod:fs.file:vf.erO8XLirRvWBRSYvGu7cAQ" deviceset="HALL" device="SOT23"/>
+<part name="GND13" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="+3V8" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="C6" library="passives" deviceset="CAP" device="0805" value="0.1uF"/>
+<part name="C7" library="passives" deviceset="CAP" device="0805" value="0.1uF"/>
+<part name="GND14" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="GND" device=""/>
+<part name="+3V9" library="supply1" library_urn="urn:adsk.eagle:library:371" deviceset="+3V3" device=""/>
+<part name="C8" library="passives" deviceset="CAP" device="0805" value="0.1uF"/>
+</parts>
+<sheets>
+<sheet>
+<plain>
+<text x="177.8" y="76.2" size="1.778" layer="97">A0!</text>
+</plain>
+<instances>
+<instance part="U1" gate="G$1" x="106.68" y="48.26" smashed="yes">
+<attribute name="NAME" x="86.3462" y="83.8578" size="1.780409375" layer="95"/>
+<attribute name="VALUE" x="86.342" y="12.646" size="1.78115" layer="96"/>
+</instance>
+<instance part="J1" gate="G$1" x="25.4" y="35.56" smashed="yes">
+<attribute name="NAME" x="12.7" y="43.434" size="1.778" layer="95" font="vector"/>
+<attribute name="VALUE" x="12.7" y="25.654" size="1.778" layer="96" font="vector"/>
+</instance>
+<instance part="P+1" gate="1" x="25.4" y="109.22" smashed="yes" rot="R270">
+<attribute name="VALUE" x="27.94" y="109.22" size="1.778" layer="96"/>
+</instance>
+<instance part="+3V1" gate="G$1" x="157.48" y="106.68" smashed="yes" rot="R270">
+<attribute name="VALUE" x="160.02" y="106.68" size="1.778" layer="96"/>
+</instance>
+<instance part="GND1" gate="1" x="17.78" y="116.84" smashed="yes" rot="R180">
+<attribute name="VALUE" x="20.32" y="119.38" size="1.778" layer="96" rot="R180"/>
+</instance>
+<instance part="GND2" gate="1" x="2.54" y="20.32" smashed="yes">
+<attribute name="VALUE" x="0" y="17.78" size="1.778" layer="96"/>
+</instance>
+<instance part="GND3" gate="1" x="73.66" y="7.62" smashed="yes">
+<attribute name="VALUE" x="71.12" y="5.08" size="1.778" layer="96"/>
+</instance>
+<instance part="+3V2" gate="G$1" x="2.54" y="50.8" smashed="yes">
+<attribute name="VALUE" x="2.54" y="53.34" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="S1" gate="G$1" x="63.5" y="22.86" smashed="yes">
+<attribute name="NAME" x="57.15" y="20.32" size="1.778" layer="95" rot="R90"/>
+<attribute name="VALUE" x="58.42" y="17.78" size="1.778" layer="96" rot="R180"/>
+</instance>
+<instance part="C1" gate="G$1" x="60.96" y="73.66" smashed="yes" rot="R270">
+<attribute name="NAME" x="63.881" y="72.136" size="1.778" layer="95" rot="R270"/>
+<attribute name="VALUE" x="58.801" y="72.136" size="1.778" layer="96" rot="R270"/>
+<attribute name="PACKAGE" x="56.896" y="72.136" size="1.27" layer="97" rot="R270"/>
+<attribute name="VOLTAGE" x="55.118" y="72.136" size="1.27" layer="97" rot="R270"/>
+<attribute name="TYPE" x="53.34" y="72.136" size="1.27" layer="97" rot="R270"/>
+</instance>
+<instance part="C2" gate="G$1" x="60.96" y="66.04" smashed="yes" rot="R270">
+<attribute name="NAME" x="63.881" y="64.516" size="1.778" layer="95" rot="R270"/>
+<attribute name="VALUE" x="58.801" y="64.516" size="1.778" layer="96" rot="R270"/>
+<attribute name="PACKAGE" x="56.896" y="64.516" size="1.27" layer="97" rot="R270"/>
+<attribute name="VOLTAGE" x="55.118" y="64.516" size="1.27" layer="97" rot="R270"/>
+<attribute name="TYPE" x="53.34" y="64.516" size="1.27" layer="97" rot="R270"/>
+</instance>
+<instance part="+3V3" gate="G$1" x="73.66" y="88.9" smashed="yes">
+<attribute name="VALUE" x="73.66" y="91.44" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="GND4" gate="1" x="48.26" y="66.04" smashed="yes" rot="R270">
+<attribute name="VALUE" x="45.72" y="68.58" size="1.778" layer="96" rot="R270"/>
+</instance>
+<instance part="+3V4" gate="G$1" x="60.96" y="55.88" smashed="yes">
+<attribute name="VALUE" x="63.5" y="55.88" size="1.778" layer="96"/>
+</instance>
+<instance part="R1" gate="G$1" x="60.96" y="45.72" smashed="yes" rot="R90">
+<attribute name="NAME" x="59.4614" y="41.91" size="1.778" layer="95" rot="R90"/>
+<attribute name="VALUE" x="64.262" y="41.91" size="1.778" layer="96" rot="R90"/>
+<attribute name="PRECISION" x="67.818" y="41.91" size="1.27" layer="97" rot="R90"/>
+<attribute name="PACKAGE" x="66.04" y="41.91" size="1.27" layer="97" rot="R90"/>
+</instance>
+<instance part="U2" gate="G$1" x="132.08" y="106.68" smashed="yes">
+<attribute name="NAME" x="121.92" y="119.38" size="1.27" layer="95" align="top-left"/>
+<attribute name="VALUE" x="121.92" y="93.98" size="1.27" layer="95"/>
+</instance>
+<instance part="U3" gate="G$1" x="132.08" y="139.7" smashed="yes">
+<attribute name="NAME" x="121.92" y="152.4" size="1.27" layer="95" align="top-left"/>
+<attribute name="VALUE" x="121.92" y="127" size="1.27" layer="95"/>
+</instance>
+<instance part="X1" gate="G$1" x="5.08" y="109.22" smashed="yes" rot="R270">
+<attribute name="NAME" x="3.175" y="113.665" size="1.27" layer="95" font="vector"/>
+<attribute name="VALUE" x="3.175" y="100.965" size="1.27" layer="96" font="vector"/>
+</instance>
+<instance part="+3V5" gate="G$1" x="157.48" y="139.7" smashed="yes" rot="R270">
+<attribute name="VALUE" x="160.02" y="139.7" size="1.778" layer="96"/>
+</instance>
+<instance part="U4" gate="G$1" x="17.78" y="78.74" smashed="yes">
+<attribute name="NAME" x="15.24" y="86.36" size="1.27" layer="95"/>
+<attribute name="VALUE" x="20.32" y="71.12" size="1.27" layer="96"/>
+</instance>
+<instance part="C3" gate="G$1" x="2.54" y="68.58" smashed="yes">
+<attribute name="NAME" x="4.064" y="71.501" size="1.778" layer="95"/>
+<attribute name="VALUE" x="4.064" y="66.421" size="1.778" layer="96"/>
+<attribute name="PACKAGE" x="4.064" y="64.516" size="1.27" layer="97"/>
+<attribute name="VOLTAGE" x="4.064" y="62.738" size="1.27" layer="97"/>
+<attribute name="TYPE" x="4.064" y="60.96" size="1.27" layer="97"/>
+</instance>
+<instance part="C4" gate="G$1" x="33.02" y="68.58" smashed="yes">
+<attribute name="NAME" x="34.544" y="71.501" size="1.778" layer="95"/>
+<attribute name="VALUE" x="34.544" y="66.421" size="1.778" layer="96"/>
+<attribute name="PACKAGE" x="34.544" y="64.516" size="1.27" layer="97"/>
+<attribute name="VOLTAGE" x="34.544" y="62.738" size="1.27" layer="97"/>
+<attribute name="TYPE" x="34.544" y="60.96" size="1.27" layer="97"/>
+</instance>
+<instance part="P+2" gate="1" x="2.54" y="88.9" smashed="yes">
+<attribute name="VALUE" x="2.54" y="91.44" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="+3V6" gate="G$1" x="33.02" y="88.9" smashed="yes">
+<attribute name="VALUE" x="33.02" y="91.44" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="GND5" gate="1" x="17.78" y="58.42" smashed="yes">
+<attribute name="VALUE" x="15.24" y="55.88" size="1.778" layer="96"/>
+</instance>
+<instance part="J2" gate="G$1" x="27.94" y="132.08" smashed="yes">
+<attribute name="VALUE" x="24.13" y="122.174" size="1.778" layer="96" font="vector"/>
+<attribute name="NAME" x="24.13" y="140.208" size="1.778" layer="95" font="vector"/>
+</instance>
+<instance part="GND6" gate="1" x="7.62" y="132.08" smashed="yes" rot="R270">
+<attribute name="VALUE" x="5.08" y="134.62" size="1.778" layer="96" rot="R270"/>
+</instance>
+<instance part="GND7" gate="1" x="48.26" y="132.08" smashed="yes" rot="R90">
+<attribute name="VALUE" x="50.8" y="129.54" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="P+3" gate="1" x="7.62" y="127" smashed="yes" rot="R90">
+<attribute name="VALUE" x="5.08" y="127" size="1.778" layer="96" rot="R180"/>
+</instance>
+<instance part="P+4" gate="1" x="48.26" y="137.16" smashed="yes" rot="R270">
+<attribute name="VALUE" x="50.8" y="137.16" size="1.778" layer="96"/>
+</instance>
+<instance part="GND8" gate="1" x="152.4" y="127" smashed="yes">
+<attribute name="VALUE" x="149.86" y="124.46" size="1.778" layer="96"/>
+</instance>
+<instance part="GND9" gate="1" x="152.4" y="93.98" smashed="yes">
+<attribute name="VALUE" x="149.86" y="91.44" size="1.778" layer="96"/>
+</instance>
+<instance part="D1" gate="G$1" x="185.42" y="33.02" smashed="yes" rot="R90">
+<attribute name="NAME" x="187.452" y="36.576" size="1.778" layer="95" rot="R180"/>
+<attribute name="VALUE" x="187.452" y="38.735" size="1.778" layer="96" rot="R180"/>
+</instance>
+<instance part="R2" gate="G$1" x="195.58" y="33.02" smashed="yes" rot="R180">
+<attribute name="NAME" x="199.39" y="31.5214" size="1.778" layer="95" rot="R180"/>
+<attribute name="VALUE" x="199.39" y="36.322" size="1.778" layer="96" rot="R180"/>
+<attribute name="PRECISION" x="199.39" y="39.878" size="1.27" layer="97" rot="R180"/>
+<attribute name="PACKAGE" x="199.39" y="38.1" size="1.27" layer="97" rot="R180"/>
+</instance>
+<instance part="GND10" gate="1" x="205.74" y="33.02" smashed="yes" rot="R90">
+<attribute name="VALUE" x="208.28" y="30.48" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="D2" gate="G$1" x="124.46" y="160.02" smashed="yes" rot="R90">
+<attribute name="NAME" x="126.492" y="163.576" size="1.778" layer="95" rot="R180"/>
+<attribute name="VALUE" x="126.492" y="165.735" size="1.778" layer="96" rot="R180"/>
+</instance>
+<instance part="R3" gate="G$1" x="134.62" y="160.02" smashed="yes" rot="R180">
+<attribute name="NAME" x="138.43" y="158.5214" size="1.778" layer="95" rot="R180"/>
+<attribute name="VALUE" x="138.43" y="163.322" size="1.778" layer="96" rot="R180"/>
+<attribute name="PRECISION" x="138.43" y="166.878" size="1.27" layer="97" rot="R180"/>
+<attribute name="PACKAGE" x="138.43" y="165.1" size="1.27" layer="97" rot="R180"/>
+</instance>
+<instance part="GND11" gate="1" x="157.48" y="160.02" smashed="yes" rot="R90">
+<attribute name="VALUE" x="160.02" y="157.48" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="S2" gate="G$1" x="124.46" y="170.18" smashed="yes" rot="R270">
+<attribute name="NAME" x="121.92" y="176.53" size="1.778" layer="95"/>
+<attribute name="VALUE" x="129.54" y="175.26" size="1.778" layer="96"/>
+</instance>
+<instance part="GND12" gate="1" x="157.48" y="167.64" smashed="yes" rot="R90">
+<attribute name="VALUE" x="160.02" y="165.1" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="R4" gate="G$1" x="71.12" y="45.72" smashed="yes" rot="R90">
+<attribute name="NAME" x="69.6214" y="41.91" size="1.778" layer="95" rot="R90"/>
+<attribute name="VALUE" x="74.422" y="41.91" size="1.778" layer="96" rot="R90"/>
+<attribute name="PRECISION" x="77.978" y="41.91" size="1.27" layer="97" rot="R90"/>
+<attribute name="PACKAGE" x="76.2" y="41.91" size="1.27" layer="97" rot="R90"/>
+</instance>
+<instance part="+3V7" gate="G$1" x="71.12" y="55.88" smashed="yes">
+<attribute name="VALUE" x="73.66" y="55.88" size="1.778" layer="96"/>
+</instance>
+<instance part="U5" gate="G$1" x="243.84" y="58.42" smashed="yes">
+<attribute name="NAME" x="241.3" y="71.12" size="1.27" layer="95"/>
+<attribute name="VALUE" x="241.3" y="45.72" size="1.27" layer="96"/>
+</instance>
+<instance part="C5" gate="G$1" x="60.96" y="81.28" smashed="yes" rot="R270">
+<attribute name="NAME" x="63.881" y="79.756" size="1.778" layer="95" rot="R270"/>
+<attribute name="VALUE" x="58.801" y="79.756" size="1.778" layer="96" rot="R270"/>
+<attribute name="PACKAGE" x="56.896" y="79.756" size="1.27" layer="97" rot="R270"/>
+<attribute name="VOLTAGE" x="55.118" y="79.756" size="1.27" layer="97" rot="R270"/>
+<attribute name="TYPE" x="53.34" y="79.756" size="1.27" layer="97" rot="R270"/>
+</instance>
+<instance part="U6" gate="G$1" x="246.38" y="27.94" smashed="yes" rot="R90">
+<attribute name="NAME" x="228.6" y="17.78" size="1.27" layer="95" rot="R90"/>
+<attribute name="VALUE" x="228.6" y="30.48" size="1.27" layer="96" rot="R90"/>
+</instance>
+<instance part="GND13" gate="1" x="261.62" y="27.94" smashed="yes" rot="R90">
+<attribute name="VALUE" x="264.16" y="25.4" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="+3V8" gate="G$1" x="261.62" y="20.32" smashed="yes" rot="R270">
+<attribute name="VALUE" x="264.16" y="20.32" size="1.778" layer="96"/>
+</instance>
+<instance part="C6" gate="G$1" x="248.92" y="22.86" smashed="yes">
+<attribute name="NAME" x="250.444" y="25.781" size="1.778" layer="95"/>
+<attribute name="VALUE" x="250.444" y="20.701" size="1.778" layer="96"/>
+<attribute name="PACKAGE" x="250.444" y="18.796" size="1.27" layer="97"/>
+<attribute name="VOLTAGE" x="250.444" y="17.018" size="1.27" layer="97"/>
+<attribute name="TYPE" x="250.444" y="15.24" size="1.27" layer="97"/>
+</instance>
+<instance part="C7" gate="G$1" x="269.24" y="58.42" smashed="yes">
+<attribute name="NAME" x="270.764" y="61.341" size="1.778" layer="95"/>
+<attribute name="VALUE" x="270.764" y="56.261" size="1.778" layer="96"/>
+<attribute name="PACKAGE" x="270.764" y="54.356" size="1.27" layer="97"/>
+<attribute name="VOLTAGE" x="270.764" y="52.578" size="1.27" layer="97"/>
+<attribute name="TYPE" x="270.764" y="50.8" size="1.27" layer="97"/>
+</instance>
+<instance part="GND14" gate="1" x="279.4" y="63.5" smashed="yes" rot="R90">
+<attribute name="VALUE" x="281.94" y="60.96" size="1.778" layer="96" rot="R90"/>
+</instance>
+<instance part="+3V9" gate="G$1" x="279.4" y="55.88" smashed="yes" rot="R270">
+<attribute name="VALUE" x="281.94" y="55.88" size="1.778" layer="96"/>
+</instance>
+<instance part="C8" gate="G$1" x="152.4" y="134.62" smashed="yes">
+<attribute name="NAME" x="153.924" y="137.541" size="1.778" layer="95"/>
+<attribute name="VALUE" x="153.924" y="132.461" size="1.778" layer="96"/>
+<attribute name="PACKAGE" x="153.924" y="130.556" size="1.27" layer="97"/>
+<attribute name="VOLTAGE" x="153.924" y="128.778" size="1.27" layer="97"/>
+<attribute name="TYPE" x="153.924" y="127" size="1.27" layer="97"/>
+</instance>
+</instances>
+<busses>
+</busses>
+<nets>
+<net name="+3V3" class="0">
+<segment>
+<pinref part="+3V2" gate="G$1" pin="+3V3"/>
+<wire x1="2.54" y1="48.26" x2="2.54" y2="40.64" width="0.1524" layer="91"/>
+<wire x1="2.54" y1="40.64" x2="10.16" y2="40.64" width="0.1524" layer="91"/>
+<pinref part="J1" gate="G$1" pin="VCC"/>
+</segment>
+<segment>
+<pinref part="C1" gate="G$1" pin="1"/>
+<wire x1="66.04" y1="73.66" x2="73.66" y2="73.66" width="0.1524" layer="91"/>
+<pinref part="U1" gate="G$1" pin="VDDANA"/>
+<pinref part="U1" gate="G$1" pin="VDDIN"/>
+<wire x1="73.66" y1="73.66" x2="81.28" y2="73.66" width="0.1524" layer="91"/>
+<wire x1="81.28" y1="81.28" x2="73.66" y2="81.28" width="0.1524" layer="91"/>
+<wire x1="73.66" y1="81.28" x2="73.66" y2="73.66" width="0.1524" layer="91"/>
+<junction x="73.66" y="73.66"/>
+<wire x1="73.66" y1="81.28" x2="73.66" y2="86.36" width="0.1524" layer="91"/>
+<junction x="73.66" y="81.28"/>
+<pinref part="+3V3" gate="G$1" pin="+3V3"/>
+<pinref part="C5" gate="G$1" pin="1"/>
+<wire x1="66.04" y1="81.28" x2="73.66" y2="81.28" width="0.1524" layer="91"/>
+</segment>
+<segment>
+<pinref part="R1" gate="G$1" pin="2"/>
+<wire x1="60.96" y1="50.8" x2="60.96" y2="53.34" width="0.1524" layer="91"/>
+<pinref part="+3V4" gate="G$1" pin="+3V3"/>
+</segment>
+<segment>
+<pinref part="U3" gate="G$1" pin="VCC"/>
+<wire x1="147.32" y1="139.7" x2="152.4" y2="139.7" width="0.1524" layer="91"/>
+<pinref part="+3V5" gate="G$1" pin="+3V3"/>
+<pinref part="C8" gate="G$1" pin="1"/>
+<wire x1="152.4" y1="139.7" x2="154.94" y2="139.7" width="0.1524" layer="91"/>
+<junction x="152.4" y="139.7"/>
+</segment>
+<segment>
+<pinref part="+3V1" gate="G$1" pin="+3V3"/>
+<wire x1="154.94" y1="106.68" x2="147.32" y2="106.68" width="0.1524" layer="91"/>
+<pinref part="U2" gate="G$1" pin="VCC"/>
+</segment>
+<segment>
+<pinref part="+3V6" gate="G$1" pin="+3V3"/>
+<wire x1="33.02" y1="86.36" x2="33.02" y2="81.28" width="0.1524" layer="91"/>
+<pinref part="C4" gate="G$1" pin="1"/>
+<wire x1="33.02" y1="73.66" x2="33.02" y2="81.28" width="0.1524" layer="91"/>
+<wire x1="33.02" y1="81.28" x2="30.48" y2="81.28" width="0.1524" layer="91"/>
+<pinref part="U4" gate="G$1" pin="VOUT"/>
+<junction x="33.02" y="81.28"/>
+</segment>
+<segment>
+<pinref part="+3V7" gate="G$1" pin="+3V3"/>
+<wire x1="71.12" y1="53.34" x2="71.12" y2="50.8" width="0.1524" layer="91"/>
+<pinref part="R4" gate="G$1" pin="2"/>
+</segment>
+<segment>
+<pinref part="+3V8" gate="G$1" pin="+3V3"/>
+<wire x1="259.08" y1="20.32" x2="248.92" y2="20.32" width="0.1524" layer="91"/>
+<pinref part="U6" gate="G$1" pin="VCC"/>
+<pinref part="C6" gate="G$1" pin="2"/>
+<wire x1="248.92" y1="20.32" x2="246.38" y2="20.32" width="0.1524" layer="91"/>
+<junction x="248.92" y="20.32"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="VDD3V"/>
+<wire x1="259.08" y1="60.96" x2="264.16" y2="60.96" width="0.1524" layer="91"/>
+<wire x1="264.16" y1="60.96" x2="264.16" y2="58.42" width="0.1524" layer="91"/>
+<wire x1="264.16" y1="58.42" x2="264.16" y2="55.88" width="0.1524" layer="91"/>
+<wire x1="264.16" y1="55.88" x2="269.24" y2="55.88" width="0.1524" layer="91"/>
+<pinref part="C7" gate="G$1" pin="2"/>
+<wire x1="269.24" y1="55.88" x2="276.86" y2="55.88" width="0.1524" layer="91"/>
+<junction x="269.24" y="55.88"/>
+<pinref part="U5" gate="G$1" pin="VDD"/>
+<wire x1="259.08" y1="58.42" x2="264.16" y2="58.42" width="0.1524" layer="91"/>
+<junction x="264.16" y="58.42"/>
+<pinref part="+3V9" gate="G$1" pin="+3V3"/>
+</segment>
+</net>
+<net name="GND" class="0">
+<segment>
+<pinref part="J1" gate="G$1" pin="GND@3"/>
+<wire x1="10.16" y1="38.1" x2="2.54" y2="38.1" width="0.1524" layer="91"/>
+<wire x1="2.54" y1="38.1" x2="2.54" y2="35.56" width="0.1524" layer="91"/>
+<pinref part="GND2" gate="1" pin="GND"/>
+<pinref part="J1" gate="G$1" pin="GND@5"/>
+<wire x1="2.54" y1="35.56" x2="2.54" y2="30.48" width="0.1524" layer="91"/>
+<wire x1="2.54" y1="30.48" x2="2.54" y2="22.86" width="0.1524" layer="91"/>
+<wire x1="10.16" y1="35.56" x2="2.54" y2="35.56" width="0.1524" layer="91"/>
+<junction x="2.54" y="35.56"/>
+<pinref part="J1" gate="G$1" pin="GNDDTCT"/>
+<wire x1="10.16" y1="30.48" x2="2.54" y2="30.48" width="0.1524" layer="91"/>
+<junction x="2.54" y="30.48"/>
+</segment>
+<segment>
+<pinref part="S1" gate="G$1" pin="P1"/>
+<wire x1="66.04" y1="17.78" x2="73.66" y2="17.78" width="0.1524" layer="91"/>
+<wire x1="73.66" y1="17.78" x2="73.66" y2="10.16" width="0.1524" layer="91"/>
+<pinref part="GND3" gate="1" pin="GND"/>
+<wire x1="73.66" y1="17.78" x2="81.28" y2="17.78" width="0.1524" layer="91"/>
+<junction x="73.66" y="17.78"/>
+<pinref part="U1" gate="G$1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="GND4" gate="1" pin="GND"/>
+<wire x1="50.8" y1="66.04" x2="53.34" y2="66.04" width="0.1524" layer="91"/>
+<pinref part="C2" gate="G$1" pin="2"/>
+<pinref part="C1" gate="G$1" pin="2"/>
+<wire x1="53.34" y1="66.04" x2="58.42" y2="66.04" width="0.1524" layer="91"/>
+<wire x1="58.42" y1="73.66" x2="53.34" y2="73.66" width="0.1524" layer="91"/>
+<wire x1="53.34" y1="73.66" x2="53.34" y2="66.04" width="0.1524" layer="91"/>
+<junction x="53.34" y="66.04"/>
+<pinref part="C5" gate="G$1" pin="2"/>
+<wire x1="58.42" y1="81.28" x2="53.34" y2="81.28" width="0.1524" layer="91"/>
+<wire x1="53.34" y1="81.28" x2="53.34" y2="73.66" width="0.1524" layer="91"/>
+<junction x="53.34" y="73.66"/>
+</segment>
+<segment>
+<pinref part="X1" gate="G$1" pin="GND"/>
+<wire x1="10.16" y1="111.76" x2="17.78" y2="111.76" width="0.1524" layer="91"/>
+<wire x1="17.78" y1="111.76" x2="17.78" y2="114.3" width="0.1524" layer="91"/>
+<pinref part="GND1" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="GND5" gate="1" pin="GND"/>
+<wire x1="17.78" y1="60.96" x2="17.78" y2="66.04" width="0.1524" layer="91"/>
+<pinref part="C3" gate="G$1" pin="2"/>
+<wire x1="2.54" y1="66.04" x2="17.78" y2="66.04" width="0.1524" layer="91"/>
+<wire x1="17.78" y1="66.04" x2="17.78" y2="68.58" width="0.1524" layer="91"/>
+<pinref part="U4" gate="G$1" pin="GND"/>
+<wire x1="17.78" y1="66.04" x2="33.02" y2="66.04" width="0.1524" layer="91"/>
+<junction x="17.78" y="66.04"/>
+<pinref part="C4" gate="G$1" pin="2"/>
+</segment>
+<segment>
+<pinref part="J2" gate="G$1" pin="5"/>
+<wire x1="20.32" y1="132.08" x2="10.16" y2="132.08" width="0.1524" layer="91"/>
+<pinref part="GND6" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="J2" gate="G$1" pin="6"/>
+<wire x1="35.56" y1="132.08" x2="45.72" y2="132.08" width="0.1524" layer="91"/>
+<pinref part="GND7" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="U3" gate="G$1" pin="GND"/>
+<wire x1="147.32" y1="132.08" x2="152.4" y2="132.08" width="0.1524" layer="91"/>
+<wire x1="152.4" y1="132.08" x2="152.4" y2="129.54" width="0.1524" layer="91"/>
+<pinref part="GND8" gate="1" pin="GND"/>
+<pinref part="C8" gate="G$1" pin="2"/>
+<junction x="152.4" y="132.08"/>
+</segment>
+<segment>
+<pinref part="U2" gate="G$1" pin="GND"/>
+<wire x1="147.32" y1="99.06" x2="152.4" y2="99.06" width="0.1524" layer="91"/>
+<wire x1="152.4" y1="99.06" x2="152.4" y2="96.52" width="0.1524" layer="91"/>
+<pinref part="GND9" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="R2" gate="G$1" pin="1"/>
+<wire x1="200.66" y1="33.02" x2="203.2" y2="33.02" width="0.1524" layer="91"/>
+<pinref part="GND10" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="R3" gate="G$1" pin="1"/>
+<wire x1="139.7" y1="160.02" x2="154.94" y2="160.02" width="0.1524" layer="91"/>
+<pinref part="GND11" gate="1" pin="GND"/>
+</segment>
+<segment>
+<pinref part="GND12" gate="1" pin="GND"/>
+<wire x1="154.94" y1="167.64" x2="129.54" y2="167.64" width="0.1524" layer="91"/>
+<pinref part="S2" gate="G$1" pin="S1"/>
+</segment>
+<segment>
+<pinref part="GND13" gate="1" pin="GND"/>
+<wire x1="259.08" y1="27.94" x2="248.92" y2="27.94" width="0.1524" layer="91"/>
+<pinref part="U6" gate="G$1" pin="GND"/>
+<pinref part="C6" gate="G$1" pin="1"/>
+<wire x1="248.92" y1="27.94" x2="246.38" y2="27.94" width="0.1524" layer="91"/>
+<junction x="248.92" y="27.94"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="GND"/>
+<wire x1="259.08" y1="63.5" x2="269.24" y2="63.5" width="0.1524" layer="91"/>
+<pinref part="C7" gate="G$1" pin="1"/>
+<wire x1="269.24" y1="63.5" x2="276.86" y2="63.5" width="0.1524" layer="91"/>
+<junction x="269.24" y="63.5"/>
+<pinref part="GND14" gate="1" pin="GND"/>
+</segment>
+</net>
+<net name="SWDIO" class="0">
+<segment>
+<pinref part="J1" gate="G$1" pin="SWDIO/TMS"/>
+<wire x1="43.18" y1="40.64" x2="58.42" y2="40.64" width="0.1524" layer="91"/>
+<label x="45.72" y="40.64" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA31/TCC1-1/SER1-3/SWDIO"/>
+<wire x1="160.02" y1="17.78" x2="175.26" y2="17.78" width="0.1524" layer="91"/>
+<label x="162.56" y="17.78" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="SWDCLK" class="0">
+<segment>
+<pinref part="J1" gate="G$1" pin="SWDCLK/TCK"/>
+<wire x1="43.18" y1="38.1" x2="60.96" y2="38.1" width="0.1524" layer="91"/>
+<pinref part="R1" gate="G$1" pin="1"/>
+<wire x1="60.96" y1="40.64" x2="60.96" y2="38.1" width="0.1524" layer="91"/>
+<label x="45.72" y="38.1" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA30/TCC1-0/SER1-2/SWDCLK"/>
+<wire x1="160.02" y1="20.32" x2="175.26" y2="20.32" width="0.1524" layer="91"/>
+<label x="162.56" y="20.32" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="RESET" class="0">
+<segment>
+<pinref part="S1" gate="G$1" pin="S1"/>
+<wire x1="66.04" y1="27.94" x2="66.04" y2="30.48" width="0.1524" layer="91"/>
+<wire x1="66.04" y1="30.48" x2="71.12" y2="30.48" width="0.1524" layer="91"/>
+<pinref part="U1" gate="G$1" pin="!RESET"/>
+<wire x1="71.12" y1="30.48" x2="81.28" y2="30.48" width="0.1524" layer="91"/>
+<wire x1="66.04" y1="30.48" x2="43.18" y2="30.48" width="0.1524" layer="91"/>
+<junction x="66.04" y="30.48"/>
+<pinref part="J1" gate="G$1" pin="!RESET"/>
+<pinref part="R4" gate="G$1" pin="1"/>
+<wire x1="71.12" y1="40.64" x2="71.12" y2="30.48" width="0.1524" layer="91"/>
+<junction x="71.12" y="30.48"/>
+<label x="45.72" y="30.48" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="N$5" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="VDDCORE"/>
+<wire x1="81.28" y1="66.04" x2="66.04" y2="66.04" width="0.1524" layer="91"/>
+<pinref part="C2" gate="G$1" pin="1"/>
+</segment>
+</net>
+<net name="+5V" class="0">
+<segment>
+<pinref part="X1" gate="G$1" pin="VBUS"/>
+<wire x1="10.16" y1="109.22" x2="22.86" y2="109.22" width="0.1524" layer="91"/>
+<pinref part="P+1" gate="1" pin="+5V"/>
+</segment>
+<segment>
+<pinref part="P+2" gate="1" pin="+5V"/>
+<wire x1="2.54" y1="86.36" x2="2.54" y2="81.28" width="0.1524" layer="91"/>
+<pinref part="U4" gate="G$1" pin="EN"/>
+<wire x1="5.08" y1="76.2" x2="2.54" y2="76.2" width="0.1524" layer="91"/>
+<wire x1="2.54" y1="76.2" x2="2.54" y2="73.66" width="0.1524" layer="91"/>
+<pinref part="C3" gate="G$1" pin="1"/>
+<pinref part="U4" gate="G$1" pin="VIN"/>
+<wire x1="5.08" y1="81.28" x2="2.54" y2="81.28" width="0.1524" layer="91"/>
+<wire x1="2.54" y1="81.28" x2="2.54" y2="76.2" width="0.1524" layer="91"/>
+<junction x="2.54" y="81.28"/>
+<junction x="2.54" y="76.2"/>
+</segment>
+<segment>
+<pinref part="J2" gate="G$1" pin="9"/>
+<wire x1="20.32" y1="127" x2="10.16" y2="127" width="0.1524" layer="91"/>
+<pinref part="P+3" gate="1" pin="+5V"/>
+</segment>
+<segment>
+<pinref part="J2" gate="G$1" pin="2"/>
+<wire x1="35.56" y1="137.16" x2="45.72" y2="137.16" width="0.1524" layer="91"/>
+<pinref part="P+4" gate="1" pin="+5V"/>
+</segment>
+</net>
+<net name="USBDM" class="0">
+<segment>
+<pinref part="X1" gate="G$1" pin="D-"/>
+<wire x1="10.16" y1="106.68" x2="20.32" y2="106.68" width="0.1524" layer="91"/>
+<label x="12.7" y="106.68" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA24/TC5-0/TCC1-2/SER3-2/USB-DM"/>
+<wire x1="160.02" y1="30.48" x2="175.26" y2="30.48" width="0.1524" layer="91"/>
+<label x="162.56" y="30.48" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="USBDP" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA25/TC5-1/TCC1-3/SER3-3/USB-DP"/>
+<wire x1="160.02" y1="27.94" x2="175.26" y2="27.94" width="0.1524" layer="91"/>
+<label x="162.56" y="27.94" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="X1" gate="G$1" pin="D+"/>
+<wire x1="10.16" y1="104.14" x2="20.32" y2="104.14" width="0.1524" layer="91"/>
+<label x="12.7" y="104.14" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="Y" class="0">
+<segment>
+<pinref part="J2" gate="G$1" pin="7"/>
+<wire x1="20.32" y1="129.54" x2="12.7" y2="129.54" width="0.1524" layer="91"/>
+<label x="15.24" y="129.54" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U2" gate="G$1" pin="A/Y"/>
+<wire x1="147.32" y1="111.76" x2="154.94" y2="111.76" width="0.1524" layer="91"/>
+<label x="149.86" y="111.76" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="B" class="0">
+<segment>
+<pinref part="J2" gate="G$1" pin="3"/>
+<wire x1="20.32" y1="134.62" x2="12.7" y2="134.62" width="0.1524" layer="91"/>
+<label x="15.24" y="134.62" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U3" gate="G$1" pin="B/Z"/>
+<wire x1="147.32" y1="147.32" x2="154.94" y2="147.32" width="0.1524" layer="91"/>
+<label x="149.86" y="147.32" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="A" class="0">
+<segment>
+<pinref part="J2" gate="G$1" pin="4"/>
+<wire x1="35.56" y1="134.62" x2="43.18" y2="134.62" width="0.1524" layer="91"/>
+<label x="38.1" y="134.62" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U3" gate="G$1" pin="A/Y"/>
+<wire x1="147.32" y1="144.78" x2="154.94" y2="144.78" width="0.1524" layer="91"/>
+<label x="149.86" y="144.78" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="Z" class="0">
+<segment>
+<pinref part="J2" gate="G$1" pin="8"/>
+<wire x1="35.56" y1="129.54" x2="43.18" y2="129.54" width="0.1524" layer="91"/>
+<label x="38.1" y="129.54" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U2" gate="G$1" pin="B/Z"/>
+<wire x1="147.32" y1="114.3" x2="154.94" y2="114.3" width="0.1524" layer="91"/>
+<label x="149.86" y="114.3" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="UCBUS_RE" class="0">
+<segment>
+<pinref part="U3" gate="G$1" pin="!RE"/>
+<wire x1="116.84" y1="144.78" x2="101.6" y2="144.78" width="0.1524" layer="91"/>
+<label x="101.6" y="144.78" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA18/TC3-0/TCC0-2/SER1-2/SER3-2"/>
+<wire x1="160.02" y1="40.64" x2="175.26" y2="40.64" width="0.1524" layer="91"/>
+<label x="162.56" y="40.64" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="UCBUS_RX" class="0">
+<segment>
+<pinref part="U3" gate="G$1" pin="RO"/>
+<wire x1="116.84" y1="147.32" x2="101.6" y2="147.32" width="0.1524" layer="91"/>
+<label x="101.6" y="147.32" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA19/TC3-1/TCC0-3/SER1-3/SER3-3"/>
+<wire x1="160.02" y1="38.1" x2="175.26" y2="38.1" width="0.1524" layer="91"/>
+<label x="162.56" y="38.1" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="UCBUS_TX" class="0">
+<segment>
+<pinref part="U2" gate="G$1" pin="DI"/>
+<wire x1="116.84" y1="101.6" x2="101.6" y2="101.6" width="0.1524" layer="91"/>
+<label x="101.6" y="101.6" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA16/TCC2-0/TCC0-6/SER1-0/SER3-0"/>
+<wire x1="160.02" y1="45.72" x2="175.26" y2="45.72" width="0.1524" layer="91"/>
+<label x="162.56" y="45.72" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="UCBUS_DE" class="0">
+<segment>
+<pinref part="U2" gate="G$1" pin="DE"/>
+<wire x1="116.84" y1="99.06" x2="101.6" y2="99.06" width="0.1524" layer="91"/>
+<label x="101.6" y="99.06" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U1" gate="G$1" pin="PA17/TCC2-1/TCC0-7/SER1-1/SER3-1"/>
+<wire x1="160.02" y1="43.18" x2="175.26" y2="43.18" width="0.1524" layer="91"/>
+<label x="162.56" y="43.18" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="UCBUS_L" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA22/TC4-0/TCC0-4/SER3-0"/>
+<wire x1="160.02" y1="35.56" x2="175.26" y2="35.56" width="0.1524" layer="91"/>
+<label x="162.56" y="35.56" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="D2" gate="G$1" pin="A"/>
+<wire x1="119.38" y1="160.02" x2="101.6" y2="160.02" width="0.1524" layer="91"/>
+<label x="101.6" y="160.02" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="LIGHT" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="(ADA_D13)PA23/TC4-1/TCC0-5/SER3-1/USB-SOF"/>
+<wire x1="160.02" y1="33.02" x2="180.34" y2="33.02" width="0.1524" layer="91"/>
+<label x="162.56" y="33.02" size="1.778" layer="95"/>
+<pinref part="D1" gate="G$1" pin="A"/>
+</segment>
+</net>
+<net name="UCBUS_B" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA28"/>
+<wire x1="160.02" y1="22.86" x2="175.26" y2="22.86" width="0.1524" layer="91"/>
+<label x="162.56" y="22.86" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="S2" gate="G$1" pin="P1"/>
+<wire x1="119.38" y1="167.64" x2="101.6" y2="167.64" width="0.1524" layer="91"/>
+<label x="101.6" y="167.64" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="N$1" class="0">
+<segment>
+<wire x1="187.96" y1="33.02" x2="190.5" y2="33.02" width="0.1524" layer="91"/>
+<pinref part="R2" gate="G$1" pin="2"/>
+<pinref part="D1" gate="G$1" pin="C"/>
+</segment>
+</net>
+<net name="N$2" class="0">
+<segment>
+<pinref part="D2" gate="G$1" pin="C"/>
+<wire x1="127" y1="160.02" x2="129.54" y2="160.02" width="0.1524" layer="91"/>
+<pinref part="R3" gate="G$1" pin="2"/>
+</segment>
+</net>
+<net name="HALL" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="(ADA_D1A1)PA02/AIN-0/DAC-0"/>
+<wire x1="160.02" y1="76.2" x2="175.26" y2="76.2" width="0.1524" layer="91"/>
+<label x="162.56" y="76.2" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U6" gate="G$1" pin="OUT"/>
+<wire x1="246.38" y1="35.56" x2="259.08" y2="35.56" width="0.1524" layer="91"/>
+<label x="248.92" y="35.56" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="0-0-MOSI" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA08/AIN16/TCC0-0/TCC1-2/SER0-0/SER2-0"/>
+<wire x1="160.02" y1="60.96" x2="175.26" y2="60.96" width="0.1524" layer="91"/>
+<label x="162.56" y="60.96" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="MOSI"/>
+<wire x1="228.6" y1="58.42" x2="215.9" y2="58.42" width="0.1524" layer="91"/>
+<label x="215.9" y="58.42" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="0-1-CLK" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA09/AIN17/TCC0-1/TCC1-3/SER0-1/SER2-1"/>
+<wire x1="160.02" y1="58.42" x2="175.26" y2="58.42" width="0.1524" layer="91"/>
+<label x="162.56" y="58.42" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="CLK"/>
+<wire x1="228.6" y1="63.5" x2="215.9" y2="63.5" width="0.1524" layer="91"/>
+<label x="215.9" y="63.5" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="0-2-CS" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA10/AIN18/TCC0-2/TCC1-0/SER0-2/SER2-2"/>
+<wire x1="160.02" y1="55.88" x2="175.26" y2="55.88" width="0.1524" layer="91"/>
+<label x="162.56" y="55.88" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="CSN"/>
+<wire x1="228.6" y1="66.04" x2="215.9" y2="66.04" width="0.1524" layer="91"/>
+<label x="215.9" y="66.04" size="1.778" layer="95"/>
+</segment>
+</net>
+<net name="0-3-MISO" class="0">
+<segment>
+<pinref part="U1" gate="G$1" pin="PA11/AIN19/TCC0-3/TCC1-1/SER0-3/SER2-3"/>
+<wire x1="160.02" y1="53.34" x2="175.26" y2="53.34" width="0.1524" layer="91"/>
+<label x="162.56" y="53.34" size="1.778" layer="95"/>
+</segment>
+<segment>
+<pinref part="U5" gate="G$1" pin="MISO"/>
+<wire x1="228.6" y1="60.96" x2="215.9" y2="60.96" width="0.1524" layer="91"/>
+<label x="215.9" y="60.96" size="1.778" layer="95"/>
+</segment>
+</net>
+</nets>
+</sheet>
+</sheets>
+</schematic>
+</drawing>
+<compatibility>
+<note version="8.2" severity="warning">
+Since Version 8.2, EAGLE supports online libraries. The ids
+of those online libraries will not be understood (or retained)
+with this version.
+</note>
+<note version="8.3" severity="warning">
+Since Version 8.3, EAGLE supports URNs for individual library
+assets (packages, symbols, and devices). The URNs of those assets
+will not be understood (or retained) with this version.
+</note>
+<note version="8.3" severity="warning">
+Since Version 8.3, EAGLE supports the association of 3D packages
+with devices in libraries, schematics, and board files. Those 3D
+packages will not be understood (or retained) with this version.
+</note>
+</compatibility>
+</eagle>
diff --git a/system/ecad/lpf-filament-sensor/routed.png b/system/ecad/lpf-filament-sensor/routed.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9750ecc938164f9d9afc8f102cdb0dbfeea9bd1
Binary files /dev/null and b/system/ecad/lpf-filament-sensor/routed.png differ
diff --git a/system/ecad/lpf-filament-sensor/schematic.png b/system/ecad/lpf-filament-sensor/schematic.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f57089438d82c0129c0b212cabbe444b4b4e937
Binary files /dev/null and b/system/ecad/lpf-filament-sensor/schematic.png differ
diff --git a/system/firmware/lpf-axl-stepper/.gitignore b/system/firmware/lpf-axl-stepper/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/system/firmware/lpf-axl-stepper/.vscode/extensions.json b/system/firmware/lpf-axl-stepper/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/system/firmware/lpf-axl-stepper/include/README b/system/firmware/lpf-axl-stepper/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/system/firmware/lpf-axl-stepper/lib/README b/system/firmware/lpf-axl-stepper/lib/README
new file mode 100644
index 0000000000000000000000000000000000000000..6debab1e8b4c3faa0d06f4ff44bce343ce2cdcbf
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/system/firmware/lpf-axl-stepper/platformio.ini b/system/firmware/lpf-axl-stepper/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..50208f0cadb2e924ce3a661e45b5c88422dc71af
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/platformio.ini
@@ -0,0 +1,14 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:adafruit_feather_m4]
+platform = atmelsam
+board = adafruit_feather_m4
+framework = arduino
diff --git a/system/firmware/lpf-axl-stepper/src/axl/README.md b/system/firmware/lpf-axl-stepper/src/axl/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..bfc556011c8bc75d03f44ef5cb5589236ccb1922
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/axl/README.md
@@ -0,0 +1,5 @@
+## Holonic Motion Control
+
+submodule, used in this [stub for modular control](https://gitlab.cba.mit.edu/jakeread/linked-dynamics/-/tree/master/holonic) ... WIP 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/axl/axl.cpp b/system/firmware/lpf-axl-stepper/src/axl/axl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..02d4fb0fb10d0b14040df1afdf27614d7d4bf9ed
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/axl/axl.cpp
@@ -0,0 +1,532 @@
+/*
+hmc.cpp
+
+holonic motion control 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "axl.h"
+#include "axl_config.h"
+#include "../indicators.h"
+#include "../osape/core/ts.h"
+#include "../osape/core/osap.h"
+
+// -------------------------------------------------------- File Scoped Vars 
+
+// check it out, big state object:
+volatile axlState_t state;
+
+// we have settings... 
+axlSettings_t settings;
+
+// queue,
+axlPlannedSegment_t queue[AXL_QUEUE_LEN];
+
+// how long is a tick ? 
+float delT = (float)AXL_TICKER_INTERVAL_US / 1000000.0F;
+
+// stash / temp var,
+vect_t dist;
+
+// messages out... 
+uint8_t segmentAckMsg[128];
+uint16_t segmentAckMsgLen = 0;
+uint8_t segmentCompleteMsg[128];
+uint16_t segmentCompleteMsgLen = 0;
+
+// halt packet, 
+uint8_t haltPck[128];
+uint16_t haltPckLen = 0;
+String haltMessage;
+
+// -------------------------------------------------------- Setup 
+
+void axl_setup(void){
+  // zero / init states, 
+  state.mode = AXL_MODE_POSITION;
+  state.queueState = AXL_QUEUESTATE_EMPTY;
+  state.segDistance = 0.0F;
+  state.segVel = 0.0F;
+  state.segAccel = 0.0F;
+  // per-axis states, 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    state.positions.axis[a] = 0.0F;
+    state.velocities.axis[a] = 0.0F;
+    state.accelerations.axis[a] = 0.0F;
+    state.target.axis[a] = 0.0F;
+    // and limits / settings, 
+    settings.accelLimits.axis[a] = 100.0F;
+    settings.velocityLimits.axis[a] = 100.0F;
+  }
+  // link the ringbuffer, 
+  for(uint8_t i = 0; i < AXL_QUEUE_LEN; i ++){
+    queue[i].indice = i;
+    if(i != AXL_QUEUE_LEN - 1) queue[i].next = &(queue[i+1]);
+    if(i != 0) queue[i].previous = &(queue[i-1]);
+  }
+  queue[0].previous = &(queue[AXL_QUEUE_LEN - 1]);
+  queue[AXL_QUEUE_LEN - 1].next = &(queue[0]);
+  // init ptrs, 
+  state.head = &(queue[0]);  // where to write-in, 
+  state.tail = &(queue[0]);  // which is ticking along... 
+}
+
+// -------------------------------------------------------- Integrators  
+
+void axl_integrator(void){
+  // else continue, 
+  state.mode == AXL_MODE_QUEUE ? axl_integrateQueue() : axl_integrateMode();
+}
+
+// -------------------------------------------------------- Mode Integrator 
+void axl_integrateMode(void){
+  switch(state.mode){
+    case AXL_MODE_POSITION:
+      for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+        // calculate distance from target, 
+        dist.axis[a] = state.target.axis[a] - state.positions.axis[a];
+        // if we're in a stopping condition, do nothing, 
+        if(abs(dist.axis[a]) < AXL_POSITIONING_EPSILON && abs(state.velocities.axis[a] < AXL_POSITIONING_VEL_EPSILON)){
+          state.accelerations.axis[a] = 0.0F;
+          state.velocities.axis[a] = 0.0F;
+          continue;
+        }
+        // otherwise, set accel dir blindly, based on sign, 
+        if(dist.axis[a] <= 0.0F){
+          state.accelerations.axis[a] = - settings.accelLimits.axis[a];
+        } else {
+          state.accelerations.axis[a] = settings.accelLimits.axis[a];
+        }
+        // now find a stopping distance... this will always be positive since (vel^2)
+        float stopDistance = (state.velocities.axis[a] * state.velocities.axis[a]) / (2.0F * settings.accelLimits.axis[a]);
+        if(stopDistance >= abs(dist.axis[a])){
+          // so accel needs to be -ve to the sign of velocity, 
+          // this is a strange formulation, better would be to get the gd signs right 
+          if(state.velocities.axis[a] <= 0.0F){
+            state.accelerations.axis[a] = settings.accelLimits.axis[a];
+          } else {
+            state.accelerations.axis[a] = -settings.accelLimits.axis[a];
+          }
+        }
+      }
+      break;
+    case AXL_MODE_VELOCITY:
+      // I'd like to do "lookahead" to set accels to zero & rates to exact-counts 
+      // at each time-step, a-la positioning epsilon code, below... and consider then 
+      // switch up the switch statements, i.e. take more out of the lower half, put it in these two sections, 
+      for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+        // if under, go up, if over, go down... 
+        if(state.velocities.axis[a] < state.target.axis[a]){
+          state.accelerations.axis[a] = settings.accelLimits.axis[a];
+        } else if (state.velocities.axis[a] > state.target.axis[a]){
+          state.accelerations.axis[a] = -settings.accelLimits.axis[a];
+        }
+        // precalculate expected speed delta 
+        float velDelta = state.accelerations.axis[a] * delT;
+        // gap between current velocity and target velocity, 
+        float velGap = state.target.axis[a] - state.velocities.axis[a];
+        // if we're going to surpass target vel, set vel = targetVel, accel = 0 
+        if(abs(velGap) < abs(velDelta)){
+          state.velocities.axis[a] = state.target.axis[a];
+          state.accelerations.axis[a] = 0.0F;
+        }
+      }
+      break;
+    case AXL_MODE_ACCEL:
+      // noop, it all just happens in the integrator,
+      break;
+    default:
+      // ?
+      break;
+  } // ------------------------------------------ End Mode Switch 
+  // -------------------------------------------- Integrate 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    // ok we have an accel, now integrate vel, 
+    state.velocities.axis[a] += state.accelerations.axis[a] * delT;
+    // guard against velocity limits... 
+    // (this could be a tite macro, is used all over... )
+    if(state.velocities.axis[a] > settings.velocityLimits.axis[a]){
+      state.velocities.axis[a] = settings.velocityLimits.axis[a];
+    } else if(state.velocities.axis[a] < -settings.velocityLimits.axis[a]){
+      state.velocities.axis[a] = -settings.velocityLimits.axis[a];
+    }
+    // now update position, 
+    float posDelta = state.velocities.axis[a] * delT;
+    // checking position overshoots:
+    if(state.mode == AXL_MODE_POSITION){
+      // if we're about to make / overshoot it, 
+      if(dist.axis[a] - posDelta < AXL_POSITIONING_EPSILON && dist.axis[a] - posDelta > 0.0F){
+        // real delta is... aka hit it direct: 
+        posDelta = dist.axis[a];
+        // zero, zero... 
+        state.velocities.axis[a] = 0.0F;
+        state.accelerations.axis[a] = 0.0F;
+      }
+    }
+    // uuuhpdate 
+    state.positions.axis[a] += posDelta;
+    axl_onPositionDelta(a, posDelta, state.positions.axis[a]);
+  } // end integrate-per-axis loop, 
+} // end mode-based integrate, 
+
+// -------------------------------------------------------- QUEUE INTEGRATOR
+void axl_integrateQueue(void){
+  // if we are in queue mode, we do this integration step... which ultimately updates the same base states 
+  // finishing the project means that these two mode-switch cleanly, i.e. when we run out of segments and 
+  // resort to some minimum-energy safety policy 
+  pickup:
+  // check if we have / are within a valid move, 
+  if(!(state.tail->isReady)) return;
+  // past this point we have at least one move to make, 
+  switch(state.queueState){
+    case AXL_QUEUESTATE_RUNNING: // we were previously running, carry on:
+      break;
+    case AXL_QUEUESTATE_EMPTY: // we were previously empty, this is new, setup the delay: 
+      state.segDistance = 0.0F;
+      state.queueState = AXL_QUEUESTATE_AWIATING_START;
+      settings.queueStartTime = millis() + settings.queueStartDelayMS;
+      return;
+    case AXL_QUEUESTATE_AWIATING_START:  // we have setup the delay, are now waiting... 
+      if(millis() > settings.queueStartTime){
+        state.queueState = AXL_QUEUESTATE_INCREMENTING;
+      }
+      return;
+    case AXL_QUEUESTATE_INCREMENTING:
+      // we've just collected a new tail, so we should do 
+      state.segVel = state.tail->vi; 
+      state.segAccel = state.tail->accel; // although this statemachine decides this again... 
+      state.queueState = AXL_QUEUESTATE_RUNNING;
+      // OSAP::debug("vi:\t" + String(state.tail->vi, 2) + "\tvf:\t" + String(state.tail->vf, 2) + "\tdst:\t" + String(state.tail->distance, 2));
+      break;
+    default:
+      break;
+  }
+  // we want to know if it's time to start stopping... so, we have 
+  // vf^2 = vi^2 + 2ad 
+  // vf^2 - vi^2 = 2ad
+  // note that accel is always +ve in the move, and here we are considering deceleration, so it flips 
+  // stopDistance will be -ve when vf > vi, i.e. when the move is accelerating... 
+  float stopDistance = ((state.tail->vf * state.tail->vf) - (state.segVel * state.segVel)) / (2 * (- state.tail->accel));
+  // if we are just past stopping distance, we should start slowing down 
+  // (ideally we would anticipate the next integral, so that we don't overshoot *at all*)
+  if(stopDistance > (state.tail->distance - state.segDistance)){
+    state.segAccel = -state.tail->accel;
+  } else {
+    state.segAccel = state.tail->accel;
+  }
+  // OSAP::debug("stopdist:\t" + String(stopDistance, 2) + "\t" + String(segTail->distance - segDistance));
+  // then we want to integrate our linear velocity,
+  state.segVel += state.segAccel * delT;
+  // no negative rates, that would be *erroneous* and also *bad* 
+  if(state.segVel < -0.001F){
+    state.segVel = 0.0F;
+    OSAP::error("negative rate " + String(state.segVel), MEDIUM);
+    return;
+  }
+  // and hit vmax ceilings, 
+  if(state.segVel > state.tail->vmax) state.segVel = state.tail->vmax;
+  // integrate per-segment position, 
+  state.segDistance += state.segVel * delT;
+  // check check, 
+  // OSAP::debug("vel: " + String(segVel, 2) + " stopDist: " + String(stopDistance, 2) + " dist:" + String(segDistance, 2));
+  // -------------------------------------------- Check Segment Completion 
+  // are we done? goto the next move,
+  if(state.segDistance >= state.tail->distance){
+    // this move gets a reset, so queue observers know it's "empty" 
+    state.tail->isReady = false;
+    state.tail->isRunning = false;
+    // was it us who was tapped for the ack ?
+    if(state.tail->returnActuator == settings.ourActuatorID){
+      // was previous ack picked up in time ? bad if not 
+      if(segmentCompleteMsgLen != 0){
+        axl_halt(AXL_HALT_MOVE_COMPLETE_NOT_PICKED);
+      }
+      // otherwise carry on, 
+      segmentCompleteMsgLen = 0;
+      // segment #, and our actuator ID... 
+      ts_writeUint32(state.tail->segmentNumber, segmentCompleteMsg, &segmentCompleteMsgLen);
+      ts_writeUint8(settings.ourActuatorID, segmentCompleteMsg, &segmentCompleteMsgLen);
+      // our current position, for remote check-in ? this is (allegedly) superfluous 
+      for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+        ts_writeFloat32(state.positions.axis[a], segmentCompleteMsg, &segmentCompleteMsgLen);
+      }
+      // that's it, the ack will be picked up... 
+    } // ------------------------------ End Move Complete Write 
+    // before we increment, stash this extra distance into the next segment... 
+    state.segDistance = state.segDistance - state.tail->distance;
+    // was it the last ?
+    boolean wasLastMove = state.tail->isLastSegment;
+    // now increment the pointer, 
+    state.tail = state.tail->next;
+    // is that ready ? then grab another, if not, set moves-complete, 
+    if(!state.tail->isReady){
+      // if this is true and our velocities != 0, we are probably starved:
+      if(!wasLastMove) axl_halt(AXL_HALT_BUFFER_STARVED);
+      // we're empty now, so 
+      state.queueState = AXL_QUEUESTATE_EMPTY;
+      // and set vels to 0, 
+      for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+        state.velocities.axis[a] = 0.0F;
+      }
+      // if we're empty, don't goto pickup, just bail... 
+      return; 
+    } else {
+      state.queueState = AXL_QUEUESTATE_INCREMENTING;
+      state.tail->isRunning = true;
+    }
+    goto pickup;
+  } // end is-move-complete section, 
+  // -------------------------------------------- Integrate -> States... 
+  #warning yeah agreed with the below, something potentially fkd with looping through two pickups in one run of this fn?
+  // like, the pickup should happen on the next tick, non ? 
+  // WARN: possible here to have two accel increases, w/ no position integration ? right ? 
+  // now update each axis' vels & accels to mirror linear states, 
+  // and integrate each position using this & the unit vector, 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    // *update* accel and velocity states... we aren't using these, just mirroring from linear rep 
+    // but we want to store them to read-out elsewhere... 
+    state.accelerations.axis[a] = state.segAccel * state.tail->unitVector.axis[a];
+    state.velocities.axis[a] = state.segVel * state.tail->unitVector.axis[a];
+    // integrate position using blind integration: segments have absolute, but not global, position info, 
+    // this is potentially fine-detail buggy, fair warning 
+    float posDelta = state.velocities.axis[a] * delT;
+    state.positions.axis[a] += posDelta;
+    axl_onPositionDelta(a, posDelta, state.positions.axis[a]);
+  }
+} // end axl_integrator() 
+
+// -------------------------------------------------------- Queue Utilities 
+
+uint32_t nextSegmentNumber = 0;
+
+void axl_addSegmentToQueue(axlPlannedSegment_t segment){
+  // get a handle for the next, 
+  if(state.head->isReady){
+    OSAP::error("on moveToQueue, looks full ahead", MEDIUM);
+    return;
+  }
+  // trust issues:
+  if(segment.distance <= 0.0F){
+    OSAP::error("zero distance move", MEDIUM);
+    return;
+  }
+  // then we just stick it in there, don't we, trusting others... 
+  // since we to state.head->isReady = true *at the end* we 
+  // won't have the integrator step into this block during an interrupt... 
+  // kind of awkward copy, innit? 
+  // anyways these are the vals we get from the net:
+  state.head->segmentNumber = segment.segmentNumber;
+  if(nextSegmentNumber != state.head->segmentNumber){
+    axl_halt(AXL_HALT_OUT_OF_ORDER_ARRIVAL);
+    haltMessage = " rx'd " + String(segment.segmentNumber) + " expected " + String(nextSegmentNumber);
+  } else {
+    nextSegmentNumber ++; 
+  }
+  state.head->returnActuator = segment.returnActuator;
+  state.head->isLastSegment = segment.isLastSegment;
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    state.head->unitVector.axis[a] = segment.unitVector.axis[a];
+  }
+  state.head->distance = segment.distance;
+  state.head->vi = segment.vi; 
+  state.head->accel = segment.accel;
+  state.head->vmax = segment.vmax;
+  state.head->vf = segment.vf;
+  // and formulate our ack message... if it was req'd 
+  if(state.head->returnActuator == settings.ourActuatorID){
+    // was previous ack picked up in time ? bad if not 
+    if(segmentAckMsgLen != 0){
+      axl_halt(AXL_HALT_MOVE_COMPLETE_NOT_PICKED);
+    }
+    // otherwise carry on & write it... 
+    segmentAckMsgLen = 0;
+    // segment #, and our actuator ID... 
+    ts_writeUint32(state.head->segmentNumber, segmentAckMsg, &segmentAckMsgLen);
+    ts_writeUint8(settings.ourActuatorID, segmentAckMsg, &segmentAckMsgLen);
+    // our current position, for remote check-in ? this is (allegedly) superfluous 
+    for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+      ts_writeFloat32(state.positions.axis[a], segmentAckMsg, &segmentAckMsgLen);
+    }
+  }
+  // and set these to allow read-out of the move
+  state.head->isReady = true;
+  state.mode = AXL_MODE_QUEUE;
+  // alright then, increment the head, right? 
+  state.head = state.head->next;
+}
+
+boolean axl_hasQueueSpace(void){
+  // if next-to-fill hasn't been set clear, it's full... 
+  if(state.head->next->isReady){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// -------------------------------------------------------- Getters 
+
+// this one's awkward... it's hefty, I'm just going to serialize it straight out, 
+uint16_t axl_getState(uint8_t* data){
+  uint16_t wptr = 0;
+  __disable_irq();
+  // vect_t's 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    ts_writeFloat32(state.positions.axis[a], data, &wptr);
+    ts_writeFloat32(state.velocities.axis[a], data, &wptr);
+    ts_writeFloat32(state.accelerations.axis[a], data, &wptr);
+    ts_writeFloat32(state.target.axis[a], data, &wptr);
+  }
+  // inter-segment state, 
+  ts_writeFloat32(state.segDistance, data, &wptr);
+  ts_writeFloat32(state.segVel, data, &wptr);
+  ts_writeFloat32(state.segAccel, data, &wptr);
+  // mode, halt state, queue state, and queue pointer positions 
+  ts_writeUint8(state.mode, data, &wptr);
+  ts_writeUint8(state.haltState, data, &wptr);
+  ts_writeUint8(state.queueState, data, &wptr);
+  ts_writeUint8(state.head->indice, data, &wptr);
+  ts_writeUint8(state.tail->indice, data, &wptr);
+  __enable_irq();
+  return wptr;
+}
+
+// useful... 
+boolean axl_isMoving(void){
+  // any velocity ? 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    if(abs(state.velocities.axis[a]) > AXL_VEL_EPSILON) return true;
+  }
+  // any acceleration ?
+  if(state.mode == AXL_MODE_ACCEL) return true;
+  // any delta to targets ?
+  if(state.mode == AXL_MODE_POSITION){
+    for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+      if(abs(state.positions.axis[a] - state.target.axis[a]) > AXL_POSITIONING_EPSILON) return true;
+    }
+  }
+  // any queued motion ?
+  if(state.mode == AXL_MODE_QUEUE){
+    if(state.tail != state.head) return true;
+  }
+  // if all pass,
+  return false;
+}
+
+uint16_t axl_getSegmentAckMsg(uint8_t* msg){
+  if(segmentAckMsgLen > 0){
+    __disable_irq();
+    memcpy(msg, segmentAckMsg, segmentAckMsgLen);
+    uint16_t len = segmentAckMsgLen;
+    segmentAckMsgLen = 0;
+    __enable_irq();
+    return len;
+  } else {
+    return 0;
+  }
+}
+
+uint16_t axl_getSegmentCompleteMsg(uint8_t* msg){
+  if(segmentCompleteMsgLen > 0){
+    __disable_irq();
+    memcpy(msg, segmentCompleteMsg, segmentCompleteMsgLen);
+    uint16_t len = segmentCompleteMsgLen;
+    segmentCompleteMsgLen = 0;
+    __enable_irq();
+    return len;
+  } else {
+    return 0;
+  }
+}
+
+// -------------------------------------------------------- Halting
+
+// request to halt all, i.e. set velocities to zero 
+// also triggers a cascade msg-out if positive halt edge 
+void axl_halt(uint8_t haltCode){
+  if(haltCode == 0) return; // noop... 
+  __disable_irq();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    state.target.axis[a] = 0.0F;
+  }
+  state.mode = AXL_MODE_VELOCITY;
+  __enable_irq();
+  // if we weren't previously halted, report and cascade if not-soft, 
+  if(state.haltState == 0){
+    OSAP::error("halting for " + String(haltCode));
+    state.haltState = haltCode;
+    // if it isn't a "soft halt" then we should cascade it, 
+    if(state.haltState != AXL_HALT_SOFT){
+      haltPck[0] = AXL_HALT_CASCADE;
+      haltPckLen = 1;
+    }
+  } 
+}
+
+uint16_t axl_getHaltPacket(uint8_t* data){
+  if(haltPckLen){
+    __disable_irq();
+    memcpy(data, haltPck, haltPckLen);
+    __enable_irq();
+    ts_writeString(haltMessage, data, &haltPckLen);
+    uint16_t len = haltPckLen;
+    haltPckLen = 0;
+    return len;
+  } else {
+    return 0;
+  }
+}
+
+// -------------------------------------------------------- Setters 
+
+void axl_setSettings(axlSettings_t _settings){
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    settings.accelLimits.axis[a] = _settings.accelLimits.axis[a];
+    settings.velocityLimits.axis[a] = _settings.velocityLimits.axis[a];
+  }
+  settings.queueStartDelayMS = _settings.queueStartDelayMS;
+  settings.queueStartTime = 0; // not really settable, that 
+  settings.ourActuatorID = _settings.ourActuatorID;
+}
+
+// set *the current* position 
+void axl_setPosition(vect_t posns){
+  // ... might be good to block this in here ? 
+  if(axl_isMoving()) return;
+  // pretty simple AFAIK? 
+  __disable_irq();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    state.positions.axis[a] = posns.axis[a];
+  }
+  __enable_irq();
+}
+
+// request to go *to* this position, from current... 
+void axl_setPositionTarget(vect_t targ){
+  // no IRQ while we operate, are going to mess w/ targets etc, 
+  // if this is slow, could stash temps & swap in, but i.e. delta might change during calculation... so 
+  __disable_irq();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    // assign new target, assuming we start from current position (won't be the case when linking) 
+    state.target.axis[a] = targ.axis[a];
+  }
+  state.mode = AXL_MODE_POSITION;
+  __enable_irq();
+}
+
+// request to go at this rate... 
+void axl_setVelocityTarget(vect_t targ){
+  __disable_irq();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    state.target.axis[a] = targ.axis[a];
+  }
+  state.mode = AXL_MODE_VELOCITY;
+  __enable_irq();
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/axl/axl.h b/system/firmware/lpf-axl-stepper/src/axl/axl.h
new file mode 100644
index 0000000000000000000000000000000000000000..e8bf0e58595c564dbbbeaa83d192573af3a51830
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/axl/axl.h
@@ -0,0 +1,127 @@
+/*
+hmc.h
+
+holonic motion control 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef AXL_COORDINATOR_H_
+#define AXL_COORDINATOR_H_
+
+#include <Arduino.h>
+#include "axl_config.h"
+
+#define AXL_MODE_ACCEL 1
+#define AXL_MODE_VELOCITY 2
+#define AXL_MODE_POSITION 3 
+#define AXL_MODE_QUEUE 4
+
+#define AXL_QUEUESTATE_EMPTY 1
+#define AXL_QUEUESTATE_AWIATING_START 2
+#define AXL_QUEUESTATE_RUNNING 3
+#define AXL_QUEUESTATE_INCREMENTING 4 
+
+#define AXL_HALT_NONE 0 
+#define AXL_HALT_SOFT 1 
+#define AXL_HALT_CASCADE 3 
+#define AXL_HALT_ACK_NOT_PICKED 4
+#define AXL_HALT_MOVE_COMPLETE_NOT_PICKED 5
+#define AXL_HALT_BUFFER_STARVED 6
+#define AXL_HALT_OUT_OF_ORDER_ARRIVAL 7 
+
+struct vect_t {
+  float axis[AXL_NUM_DOF];
+};
+
+typedef struct axlPlannedSegment_t {
+  // we're given these data over the network:
+  uint32_t segmentNumber = 0;               // continuity counter 
+  uint8_t returnActuator = 0;               // which of ye will ack ? 
+  boolean isLastSegment = false;            // should we expect end of queue here ? 
+  vect_t unitVector;                        // unit vector, direction
+  float vi = 0.0F;                          // start velocity 
+  float accel = 0.0F;                       // accel rate 
+  float vmax = 0.0F;                      // max rate 
+  float vf = 0.0F;                          // end velocity 
+  float distance = 0.0F;                    // how far total 
+  // queueing flags... 
+  boolean isReady = false;                  // is... next up, needs to be executed, 
+  boolean isRunning = false;                // currently ticking, 
+  // linked list next, previous, and pos
+  axlPlannedSegment_t* next = nullptr;         // will link these... 
+  axlPlannedSegment_t* previous = nullptr;     // ... 
+  uint8_t indice = 0;                       // it can be nice to know, for list debuggen
+} axlPlannedSegment_t;
+
+typedef struct axlState_t {
+  // these triplets, always:
+  vect_t positions;
+  vect_t velocities;
+  vect_t accelerations;
+  // halt state 
+  uint8_t haltState = AXL_HALT_NONE;
+  // mode: vel, position, queue,
+  uint8_t mode;
+  uint8_t queueState;
+  // queue-al distance in segment, 
+  float segDistance;
+  float segVel;
+  float segAccel;
+  // modal target, 
+  vect_t target;
+  // queue state, 
+  axlPlannedSegment_t* head;  // ingest at 
+  axlPlannedSegment_t* tail;  // execute at 
+} axlState_t;
+
+typedef struct axlSettings_t {
+  // dynamics limits 
+  vect_t accelLimits;
+  vect_t velocityLimits;
+  // queue kit 
+  uint32_t queueStartDelayMS = 500;
+  uint32_t queueStartTime = 0; 
+  // id'ing, 
+  uint8_t ourActuatorID = 0;
+} axlSettings_t;
+
+void axl_setup(void);
+void axl_integrator(void);
+void axl_integrateMode(void);
+void axl_integrateQueue(void);
+
+// ute for apps (motors)
+void axl_onPositionDelta(uint8_t axis, float delta, float absolute);
+
+// queue... stuff ?
+boolean axl_hasQueueSpace(void);
+void axl_addSegmentToQueue(axlPlannedSegment_t move);
+uint16_t axl_getSegmentAckMsg(uint8_t* msg);
+uint16_t axl_getSegmentCompleteMsg(uint8_t* msg);
+
+// halt stuff...
+void axl_halt(void);
+void axl_halt(uint8_t haltCode);
+uint16_t axl_getHaltPacket(uint8_t* data);
+
+// set current position == this... 
+void axl_setPosition(vect_t posns);
+// seems likely we'll use these just for modal stuff, but... 
+void axl_setSettings(axlSettings_t _settings);
+
+// modal requests, could be more like "setState" - but that's TMI 
+void axl_setPositionTarget(vect_t targ);
+void axl_setVelocityTarget(vect_t targ);
+
+// get the whole chunk 
+uint16_t axl_getState(uint8_t* data);
+boolean axl_isMoving(void);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/axl/axl_config.h b/system/firmware/lpf-axl-stepper/src/axl/axl_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..695553c3acfd50b832a150bdfcb13faf374ae911
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/axl/axl_config.h
@@ -0,0 +1,32 @@
+/*
+axl_config.h
+
+holonic motion control 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef AXL_CONFIG_H_
+#define AXL_CONFIG_H_
+
+// rate for integrator 
+#define AXL_TICKER_INTERVAL_US 100
+// number of DOF total,
+#define AXL_NUM_DOF 4
+// maximum length of the embedded queue 
+#define AXL_QUEUE_LEN 32
+// epsilon for positioning moves... i.e. when to stop / not-accel, 
+#define AXL_POSITIONING_EPSILON 0.01F
+#define AXL_POSITIONING_VEL_EPSILON 0.01F
+#define AXL_VEL_EPSILON 0.01F
+
+#define AXL_DEBUG_ADDMOVE false
+#define AXL_DEBUG_QUEUES false 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/drivers/dacs.cpp b/system/firmware/lpf-axl-stepper/src/drivers/dacs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b9dbf277cb341d4d1c36240456da9fd240eaa5c1
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/drivers/dacs.cpp
@@ -0,0 +1,122 @@
+/*
+osap/drivers/dacs.cpp
+
+dacs on the d51
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "dacs.h"
+//#include "ucbus_drop.h"
+
+DACs* DACs::instance = 0;
+
+DACs* DACs::getInstance(void){
+    if(instance == 0){
+        instance = new DACs();
+    }
+    return instance;
+}
+
+DACs* dacs = DACs::getInstance();
+
+DACs::DACs() {}
+
+void DACs::init(){
+    /*
+    // the below code was an attempt to scrape from 
+    // scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/startup.c (clock)
+    // scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/wiring.c (peripheral clock)
+    // scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/wiring_analog.c
+    // to setup the DAC 'from scratch' - of course it occurred to me later that this 
+    // setup already happens in arduino's boot. so I omitted this and just used 
+    // the messy per-analogWrite-call config below, and wrote small write-to-dac functions 
+    // to operate under the assumption that this init happens once.
+
+    // ... 
+    // put the pins on the peripheral,
+    // DAC0 is PA02, Peripheral B
+    // DAC1 is PA05, Peripheral B
+    //PORT->Group[0].DIRSET.reg = (uint32_t)(1 << 2);
+    //PORT->Group[0].DIRCLR.reg = (uint32_t)(1 << 2);
+    PORT->Group[0].PINCFG[2].bit.PMUXEN = 1;
+    PORT->Group[0].PMUX[2 >> 1].reg |= PORT_PMUX_PMUXE(1);
+    //PORT->Group[0].DIRSET.reg = (uint32_t)(1 << 5);
+    //PORT->Group[0].DIRCLR.reg = (uint32_t)(1 << 5);
+    PORT->Group[0].PINCFG[5].bit.PMUXEN = 1;
+    PORT->Group[0].PMUX[5 >> 1].reg |= PORT_PMUX_PMUXO(1);
+
+    // unmask the DAC peripheral
+    MCLK->APBDMASK.bit.DAC_ = 1;
+
+    // DAC needs a clock, 
+    GCLK->GENCTRL[GENERIC_CLOCK_GENERATOR_12M].reg = GCLK_GENCTRL_SRC_DFLL | 
+        GCLK_GENCTRL_IDC | 
+        GCLK_GENCTRL_DIV(4) |
+        GCLK_GENCTRL_GENEN;
+    while(GCLK->SYNCBUSY.reg & GENERIC_CLOCK_GENERATOR_12M_SYNC);
+    // feed that clock to the DAC,
+    GCLK->PCHCTRL[DAC_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(GENERIC_CLOCK_GENERATOR_12M_SYNC);
+    while(GCLK->PCHCTRL[DAC_GCLK_ID].bit.CHEN == 0);
+    
+    // software reset the DAC 
+    while(DAC->SYNCBUSY.bit.SWRST == 1);
+    DAC->CTRLA.bit.SWRST = 1;
+    while(DAC->SYNCBUSY.bit.SWRST == 1);
+    // and finally the DAC itself, 
+    while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+    DAC->CTRLA.bit.ENABLE = 0;
+    // enable both channels 
+    while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+    DAC->DACCTRL[0].reg = DAC_DACCTRL_ENABLE | DAC_DACCTRL_REFRESH(2);
+    while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+    DAC->DACCTRL[1].reg = DAC_DACCTRL_ENABLE | DAC_DACCTRL_REFRESH(2);
+    // voltage out, and select vref
+    DAC->CTRLB.reg = DAC_CTRLB_REFSEL_VDDANA;
+    // re-enable dac 
+    while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+    DAC->CTRLA.bit.ENABLE = 1;
+    // await up, 
+    while(!DAC->STATUS.bit.READY0);
+    while(!DAC->STATUS.bit.READY1);
+    */
+   while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+   DAC->CTRLA.bit.ENABLE = 0;
+   while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+   DAC->DACCTRL[0].bit.ENABLE = 1;
+   DAC->DACCTRL[1].bit.ENABLE = 1;
+   while(DAC->SYNCBUSY.bit.ENABLE || DAC->SYNCBUSY.bit.SWRST);
+   DAC->CTRLA.bit.ENABLE = 1;
+   while(!DAC->STATUS.bit.READY0);
+   while(!DAC->STATUS.bit.READY1);
+}
+
+// 0 - 4095
+void DACs::writeDac0(uint16_t val){
+    //analogWrite(A0, val);
+    while(DAC->SYNCBUSY.bit.DATA0);
+    DAC->DATA[0].reg = val;//DAC_DATA_DATA(val);
+    currentVal0 = val;
+}
+
+void DACs::writeDac1(uint16_t val){
+    //analogWrite(A1, val);
+    while(DAC->SYNCBUSY.bit.DATA1);
+    DAC->DATA[1].reg = val;//DAC_DATA_DATA(val);
+    currentVal1 = val;
+}
+
+void DACs::refresh(void){
+    writeDac0(currentVal0);
+    writeDac1(currentVal1);
+    uint32_t now = micros();
+    if(now > lastRefresh + 1000){
+        lastRefresh = now;
+    }
+}
diff --git a/system/firmware/lpf-axl-stepper/src/drivers/dacs.h b/system/firmware/lpf-axl-stepper/src/drivers/dacs.h
new file mode 100644
index 0000000000000000000000000000000000000000..9aba2d9a8a422560702ea4cc0dcecd697c0dae09
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/drivers/dacs.h
@@ -0,0 +1,54 @@
+/*
+osap/drivers/dacs.h
+
+dacs on the d51
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef DACS_H_
+#define DACS_H_
+
+#include <arduino.h>
+
+#include "indicators.h"
+
+// scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/wiring_analog.c
+// scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/startup.c (clock)
+// scrape https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/wiring.c (peripheral clock)
+// DAC0 is on PA02
+// DAC1 is on PA05
+
+// NOTE: the DAC must be refreshed manually to maintain voltage.
+// there does appear to be a refresh register in DACCTRL band, 
+// but it does *not* seem to work... 
+
+#define GENERIC_CLOCK_GENERATOR_12M       (4u)
+#define GENERIC_CLOCK_GENERATOR_12M_SYNC   GCLK_SYNCBUSY_GENCTRL4
+
+class DACs {
+   private:
+    // is driver, is singleton, 
+    static DACs* instance;
+    volatile uint16_t currentVal0 = 0;
+    volatile uint16_t currentVal1 = 0;
+    volatile uint32_t lastRefresh = 0;
+
+   public:
+    DACs();
+    static DACs* getInstance(void);
+    void init(void);
+    void writeDac0(uint16_t val);
+    void writeDac1(uint16_t val);
+    void refresh(void);
+};
+
+extern DACs* dacs;
+
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/drivers/peripheral_nums.h b/system/firmware/lpf-axl-stepper/src/drivers/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/drivers/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.cpp b/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..440e5a8678dd941ce5280e5a8104661e94c09d7b
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.cpp
@@ -0,0 +1,214 @@
+/*
+osap/drivers/step_a4950.cpp
+
+stepper code for two A4950s
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "step_a4950.h"
+//#include "ucbus_drop.h"
+
+// sine, 0-8190, 4095 center / 'zero', 256 steps 
+uint16_t LUT_8190[256] = {
+    4095,4195,4296,4396,4496,4596,4696,4795,4894,4992,
+    5090,5187,5284,5380,5475,5569,5662,5754,5846,5936,
+    6025,6113,6200,6286,6370,6453,6534,6614,6693,6770,
+    6845,6919,6991,7061,7129,7196,7260,7323,7384,7443,
+    7500,7555,7607,7658,7706,7753,7797,7839,7878,7916,
+    7951,7983,8014,8042,8067,8091,8111,8130,8146,8159,
+    8170,8179,8185,8189,8190,8189,8185,8179,8170,8159,
+    8146,8130,8111,8091,8067,8042,8014,7983,7951,7916,
+    7878,7839,7797,7753,7706,7658,7607,7555,7500,7443,
+    7384,7323,7260,7196,7129,7061,6991,6919,6845,6770,
+    6693,6614,6534,6453,6370,6286,6200,6113,6025,5936,
+    5846,5754,5662,5569,5475,5380,5284,5187,5090,4992,
+    4894,4795,4696,4596,4496,4396,4296,4195,4095,3995,
+    3894,3794,3694,3594,3494,3395,3296,3198,3100,3003,
+    2906,2810,2715,2621,2528,2436,2344,2254,2165,2077,
+    1990,1904,1820,1737,1656,1576,1497,1420,1345,1271,
+    1199,1129,1061,994,930,867,806,747,690,635,
+    583,532,484,437,393,351,312,274,239,207,
+    176,148,123,99,79,60,44,31,20,11,
+    5,1,0,1,5,11,20,31,44,60,
+    79,99,123,148,176,207,239,274,312,351,
+    393,437,484,532,583,635,690,747,806,867,
+    930,994,1061,1129,1199,1271,1345,1420,1497,1576,
+    1656,1737,1820,1904,1990,2077,2165,2254,2344,2436,
+    2528,2621,2715,2810,2906,3003,3100,3198,3296,3395,
+    3494,3594,3694,3794,3894,3995
+};
+
+// sine, 0-1022 (511 center / 'zero'), 256 steps 
+uint16_t LUT_1022[256] = {
+    511,524,536,549,561,574,586,598,611,623,635,647,659,671,683,695,
+    707,718,729,741,752,763,774,784,795,805,815,825,835,845,854,863,
+    872,881,890,898,906,914,921,929,936,943,949,956,962,967,973,978,
+    983,988,992,996,1000,1003,1007,1010,1012,1014,1016,1018,1020,1021,1021,1022,
+    1022,1022,1021,1021,1020,1018,1016,1014,1012,1010,1007,1003,1000,996,992,988,
+    983,978,973,967,962,956,949,943,936,929,921,914,906,898,890,881,
+    872,863,854,845,835,825,815,805,795,784,774,763,752,741,729,718,
+    707,695,683,671,659,647,635,623,611,598,586,574,561,549,536,524,
+    511,498,486,473,461,448,436,424,411,399,387,375,363,351,339,327,
+    315,304,293,281,270,259,248,238,227,217,207,197,187,177,168,159,
+    150,141,132,124,116,108,101,93,86,79,73,66,60,55,49,44,
+    39,34,30,26,22,19,15,12,10,8,6,4,2,1,1,0,
+    0,0,1,1,2,4,6,8,10,12,15,19,22,26,30,34,
+    39,44,49,55,60,66,73,79,86,93,101,108,116,124,132,141,
+    150,159,168,177,187,197,207,217,227,238,248,259,270,281,293,304,
+    315,327,339,351,363,375,387,399,411,424,436,448,461,473,486,498,
+};
+
+uint16_t dacLUT[256];
+
+STEP_A4950* STEP_A4950::instance = 0;
+
+STEP_A4950* STEP_A4950::getInstance(void){
+    if(instance == 0){
+        instance = new STEP_A4950();
+    }
+    return instance;
+}
+
+STEP_A4950* stepper_hw = STEP_A4950::getInstance();
+
+STEP_A4950::STEP_A4950() {}
+
+void STEP_A4950::init(boolean invert, float cscale){
+    // all of 'em, outputs 
+    AIN1_PORT.DIRSET.reg = AIN1_BM;
+    AIN2_PORT.DIRSET.reg = AIN2_BM;
+    BIN1_PORT.DIRSET.reg = BIN1_BM;
+    BIN2_PORT.DIRSET.reg = BIN2_BM;
+    // floating cscale 
+    if(cscale < 0){
+        _cscale = 0;
+    } else if (cscale > 1){
+        _cscale = 1;
+    } else {
+        _cscale = cscale;
+    }
+    // write a rectified LUT for writing to DACs
+    for(uint16_t i = 0; i < 256; i ++){
+        if(LUT_8190[i] > 4095){
+            dacLUT[i] = LUT_8190[i] - 4095;
+        } else if (LUT_8190[i] < 4095){
+            dacLUT[i] = abs(4095 - LUT_8190[i]);
+        } else {
+            dacLUT[i] = 0;
+        }
+    }
+    // invert direction / not 
+    _dir_invert = invert;
+    // start the DAAAC
+    dacs->init();
+    // start condition, 
+    step();
+}
+
+// sequence like
+// S: 1 2 3 4 5 6 7 8 
+// A: ^ ^ ^ x v v v x
+// B: ^ x v v v x ^ ^
+void STEP_A4950::step(void){
+    // increment: wrapping comes for free with uint8_t, bless 
+    if(_dir){
+        if(_dir_invert){
+            _aStep -= _microstep_count;
+            _bStep -= _microstep_count;
+        } else {
+            _aStep += _microstep_count;
+            _bStep += _microstep_count;
+        }
+    } else {
+        if(_dir_invert){
+            _aStep += _microstep_count;
+            _bStep += _microstep_count;
+        } else {
+            _aStep -= _microstep_count;
+            _bStep -= _microstep_count;
+        }
+    }
+    // a phase, 
+    if(LUT_8190[_aStep] > 4095){
+        A_UP;
+    } else if (LUT_8190[_aStep] < 4095){
+        A_DOWN;
+    } else {
+        A_OFF;
+    }
+    // a DAC 
+    // so that we can easily rewrite currents on the fly. will extend to servoing, yeah 
+    dacs->writeDac0(dacLUT[_aStep] * _cscale);
+    // b phase, 
+    if(LUT_8190[_bStep] > 4095){
+        B_UP;
+    } else if (LUT_8190[_bStep] < 4095){
+        B_DOWN;
+    } else {
+        B_OFF;
+    }
+    // b DAC
+    dacs->writeDac1(dacLUT[_bStep] * _cscale);
+}
+
+void STEP_A4950::dir(boolean val){
+    _dir = val;
+}
+
+boolean STEP_A4950::getDir(void){
+    return _dir;
+}
+
+void STEP_A4950::setMicrostep(uint8_t microstep){
+    switch(microstep){
+        case 64:
+            _microstep_count = MICROSTEP_64_COUNT;
+            break;
+        case 32:
+            _microstep_count = MICROSTEP_32_COUNT;
+            break;
+        case 16:
+            _microstep_count = MICROSTEP_16_COUNT;
+            break;
+        case 8:
+            _microstep_count = MICROSTEP_8_COUNT;
+            break;
+        case 4: 
+            _microstep_count = MICROSTEP_4_COUNT;
+            break;
+        case 1:
+            _microstep_count = MICROSTEP_1_COUNT;
+            break;
+        default:
+            _microstep_count = MICROSTEP_1_COUNT;
+            break;
+    }
+}
+
+void STEP_A4950::setCurrent(float cscale){
+    if(cscale > 1){
+        _cscale = 1;
+    } else if(cscale < 0){
+        _cscale = 0;
+    } else {
+        _cscale = cscale;
+    }
+    // do DAC re-writes 
+    dacs->writeDac0(dacLUT[_aStep] * _cscale);
+    dacs->writeDac1(dacLUT[_bStep] * _cscale);
+}
+
+void STEP_A4950::setInversion(boolean inv){
+    _dir_invert = inv;
+}
+
+void STEP_A4950::dacRefresh(void){
+    dacs->refresh();
+}
diff --git a/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.h b/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.h
new file mode 100644
index 0000000000000000000000000000000000000000..fb19f2f12c2d651dbf52826d16f6f3fb311b4b9c
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/drivers/step_a4950.h
@@ -0,0 +1,114 @@
+/*
+osap/drivers/step_a4950.h
+
+stepper code for two A4950s
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STEP_A4950_H_
+#define STEP_A4950_H_
+
+#include <Arduino.h>
+
+#include "dacs.h"
+#include "indicators.h"
+
+// C_SCALE 
+// 1: DACs go 0->512 (of 4096, peak current is 1.6A at 4096): 0.2A
+// 2: DACs go 0->1024,
+// ...
+// 8: DACs go full width 
+//#define C_SCALE 8 // on init 
+// MICROSTEP_COUNT 
+// 1:   do 1 tick of 256 table, for full resolution, this is 64 'microsteps'
+// 2:   32 microsteps
+// 4:   16 microsteps
+// 8:   8 microsteps
+// 16:  4 microsteps
+// 32:  2 microsteps (half steps)
+// 64:  full steps 
+#define MICROSTEP_COUNT 1
+
+#define MICROSTEP_64_COUNT 1
+#define MICROSTEP_32_COUNT 2
+#define MICROSTEP_16_COUNT 4
+#define MICROSTEP_8_COUNT 8 
+#define MICROSTEP_4_COUNT 16
+#define MICROSTEP_2_COUNT 32
+#define MICROSTEP_1_COUNT 64 
+
+// AIN1 PB06
+// AIN2 PA04 
+// BIN1 PA07 
+// BIN2 PA06 
+#define AIN1_PIN 6
+#define AIN1_PORT PORT->Group[1]
+#define AIN1_BM (uint32_t)(1 << AIN1_PIN)
+#define AIN2_PIN 4 
+#define AIN2_PORT PORT->Group[0]
+#define AIN2_BM (uint32_t)(1 << AIN2_PIN)
+#define BIN1_PIN 7 
+#define BIN1_PORT PORT->Group[0]
+#define BIN1_BM (uint32_t)(1 << BIN1_PIN)
+#define BIN2_PIN 6 
+#define BIN2_PORT PORT->Group[0] 
+#define BIN2_BM (uint32_t)(1 << BIN2_PIN)
+
+// handles
+#define AIN1_HI AIN1_PORT.OUTSET.reg = AIN1_BM
+#define AIN1_LO AIN1_PORT.OUTCLR.reg = AIN1_BM
+#define AIN2_HI AIN2_PORT.OUTSET.reg = AIN2_BM
+#define AIN2_LO AIN2_PORT.OUTCLR.reg = AIN2_BM 
+#define BIN1_HI BIN1_PORT.OUTSET.reg = BIN1_BM
+#define BIN1_LO BIN1_PORT.OUTCLR.reg = BIN1_BM
+#define BIN2_HI BIN2_PORT.OUTSET.reg = BIN2_BM
+#define BIN2_LO BIN2_PORT.OUTCLR.reg = BIN2_BM
+
+// set a phase up or down direction
+// transition low first, avoid brake condition for however many ns 
+#define A_UP AIN2_LO; AIN1_HI
+#define A_OFF AIN2_LO; AIN1_LO
+#define A_DOWN AIN1_LO; AIN2_HI
+#define B_UP BIN2_LO; BIN1_HI 
+#define B_OFF BIN2_LO; BIN1_LO
+#define B_DOWN BIN1_LO; BIN2_HI
+
+class STEP_A4950 {
+   private:
+    // is driver, is singleton, 
+    static STEP_A4950* instance;
+    volatile uint8_t _aStep = 0;    // 0 of 256 micros, 
+    volatile uint8_t _bStep = 63;   // of the same table, startup 90' out of phase 
+    volatile boolean _dir = false;
+    boolean _dir_invert = false;
+    uint8_t _microstep_count = 1;
+    // try single scalar
+    float _cscale = 0.1;
+
+   public:
+    STEP_A4950();
+    static STEP_A4950* getInstance(void);
+    // do like 
+    void init(boolean invert, float cscale);
+    void step(void);
+    void dir(boolean val);
+    boolean getDir(void);
+    // microstep setting 
+    void setMicrostep(uint8_t microstep);
+    // current settings 
+    void setCurrent(float cscale);
+    void setInversion(boolean inv);
+    // for the dacs 
+    void dacRefresh(void);
+};
+
+extern STEP_A4950* stepper_hw;
+
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/indicators.h b/system/firmware/lpf-axl-stepper/src/indicators.h
new file mode 100644
index 0000000000000000000000000000000000000000..5d7452112ea3debaa5eb0b8128a73a66e150f953
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/indicators.h
@@ -0,0 +1,59 @@
+// indicators for the macrofab-d 
+#define CLKLIGHT_PIN 27
+#define CLKLIGHT_PORT PORT->Group[0]
+#define ERRLIGHT_PIN 8
+#define ERRLIGHT_PORT PORT->Group[1]
+
+// PB05, is DIP1 
+#define DEBUG1PIN_PIN 5
+#define DEBUG1PIN_PORT PORT->Group[1]
+// PA23, is limit 
+//#define DEBUG1PIN_PIN 23 
+//#define DEBUG1PIN_PORT PORT->Group[0]
+// PB04, is DIP2 
+#define DEBUG2PIN_PIN 4
+#define DEBUG2PIN_PORT PORT->Group[1]
+// NOT setup 
+#define DEBUG3PIN_PIN 13 
+#define DEBUG3PIN_PORT PORT->Group[1]
+#define DEBUG4PIN_PIN 14
+#define DEBUG4PIN_PORT PORT->Group[1]
+
+// PA27
+#define CLKLIGHT_BM (uint32_t)(1 << CLKLIGHT_PIN)
+#define CLKLIGHT_ON CLKLIGHT_PORT.OUTCLR.reg = CLKLIGHT_BM
+#define CLKLIGHT_OFF CLKLIGHT_PORT.OUTSET.reg = CLKLIGHT_BM
+#define CLKLIGHT_TOGGLE CLKLIGHT_PORT.OUTTGL.reg = CLKLIGHT_BM
+#define CLKLIGHT_SETUP CLKLIGHT_PORT.DIRSET.reg = CLKLIGHT_BM; CLKLIGHT_OFF
+
+// PB08 
+#define ERRLIGHT_BM (uint32_t)(1 << ERRLIGHT_PIN)
+#define ERRLIGHT_ON ERRLIGHT_PORT.OUTCLR.reg = ERRLIGHT_BM
+#define ERRLIGHT_OFF ERRLIGHT_PORT.OUTSET.reg = ERRLIGHT_BM
+#define ERRLIGHT_TOGGLE ERRLIGHT_PORT.OUTTGL.reg = ERRLIGHT_BM
+#define ERRLIGHT_SETUP ERRLIGHT_PORT.DIRSET.reg = ERRLIGHT_BM; ERRLIGHT_OFF
+
+// the limit: turn off as input if using as output 
+#define DEBUG1PIN_BM (uint32_t)(1 << DEBUG1PIN_PIN)
+#define DEBUG1PIN_ON DEBUG1PIN_PORT.OUTSET.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_OFF DEBUG1PIN_PORT.OUTCLR.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_TOGGLE DEBUG1PIN_PORT.OUTTGL.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_SETUP DEBUG1PIN_PORT.DIRSET.reg = DEBUG1PIN_BM; DEBUG1PIN_OFF
+
+#define DEBUG2PIN_BM (uint32_t)(1 << DEBUG2PIN_PIN)
+#define DEBUG2PIN_ON DEBUG2PIN_PORT.OUTSET.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_OFF DEBUG2PIN_PORT.OUTCLR.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_TOGGLE DEBUG2PIN_PORT.OUTTGL.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_SETUP DEBUG2PIN_PORT.DIRSET.reg = DEBUG2PIN_BM; DEBUG2PIN_OFF
+
+#define DEBUG3PIN_BM (uint32_t)(1 << DEBUG3PIN_PIN)
+#define DEBUG3PIN_ON DEBUG3PIN_PORT.OUTSET.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_OFF DEBUG3PIN_PORT.OUTCLR.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_TOGGLE DEBUG3PIN_PORT.OUTTGL.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_SETUP DEBUG3PIN_PORT.DIRSET.reg = DEBUG3PIN_BM; DEBUG3PIN_OFF
+
+#define DEBUG4PIN_BM (uint32_t)(1 << DEBUG4PIN_PIN)
+#define DEBUG4PIN_ON DEBUG4PIN_PORT.OUTSET.reg = DEBUG4PIN_BM
+#define DEBUG4PIN_OFF DEBUG4PIN_PORT.OUTCLR.reg = DEBUG4PIN_BM
+#define DEBUG4PIN_TOGGLE DEBUG4PIN_PORT.OUTTGL.reg = DEBUG4PIN_BM
+#define DEBUG4PIN_SETUP DEBUG4PIN_PORT.DIRSET.reg = DEBUG4PIN_BM; DEBUG4PIN_OFF
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/main.cpp b/system/firmware/lpf-axl-stepper/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..54f6fccbfd55acb1460a53aca7392b29b443edd2
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/main.cpp
@@ -0,0 +1,347 @@
+#include <Arduino.h>
+#include "indicators.h"
+
+#include "drivers/step_a4950.h"
+#include "axl/axl.h"
+#include "axl/axl_config.h"
+#include "utils_samd51/clock_utils.h"
+
+#include "osape/core/osap.h"
+#include "osape/core/ts.h"
+#include "osape/vertices/endpoint.h"
+#include "osape_arduino/vp_arduinoSerial.h"
+#include "osape_ucbus/vb_ucBusDrop.h"
+
+// OSAP osap("axl-stepper_z-rear-left");
+// OSAP osap("axl-stepper_z-front-left");
+// OSAP osap("axl-stepper_z-rear-right");
+// OSAP osap("axl-stepper_z-front-right");
+// OSAP osap("axl-stepper_y-left");
+// OSAP osap("axl-stepper_y-right");
+// OSAP osap("axl-stepper_x");
+OSAP osap("axl-stepper_e");
+// OSAP osap("axl-stepper_z");
+// OSAP osap("axl-stepper_rl");
+// OSAP osap("axl-stepper_rr");
+
+// -------------------------------------------------------- 0: USB Serial 
+
+VPort_ArduinoSerial vpUSBSerial(&osap, "arduinoUSBSerial", &Serial);
+
+// -------------------------------------------------------- 1: Bus Drop 
+
+VBus_UCBusDrop vbUCBusDrop(&osap, "ucBusDrop"); 
+
+// -------------------------------------------------------- 2: AXL Settings
+
+EP_ONDATA_RESPONSES onAXLSettingsData(uint8_t* data, uint16_t len){
+  // jd, then pairs of accel & vel limits,
+  axlSettings_t settings;
+  uint16_t rptr = 0;
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    settings.accelLimits.axis[a] = ts_readFloat32(data, &rptr);
+    settings.velocityLimits.axis[a] = ts_readFloat32(data, &rptr);
+  }
+  settings.queueStartDelayMS = ts_readUint32(data, &rptr);
+  settings.ourActuatorID = ts_readUint8(data, &rptr);
+  // ship em... 
+  axl_setSettings(settings);
+  // don't stash data, 
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint axlSettingsEP(&osap, "axlSettings", onAXLSettingsData);
+
+// -------------------------------------------------------- 3: Axl Modal Requests 
+
+EP_ONDATA_RESPONSES onStateData(uint8_t* data, uint16_t len){
+  // check for partner-config badness, 
+  if(len != AXL_NUM_DOF * 4 + 2){ OSAP::error("state req has bad DOF count"); return EP_ONDATA_REJECT; }
+  // we have accel, rate, posn data, 
+  vect_t targ;
+  uint16_t rptr = 0;
+  uint8_t mode = data[rptr ++];
+  uint8_t set = data[rptr ++];
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    targ.axis[a] = ts_readFloat32(data, &rptr);
+  }
+  // set or target?
+  if(set){
+    switch(mode){
+      case AXL_MODE_POSITION:
+        if(axl_isMoving()){
+          OSAP::error("AXL can't set pos while moving");
+          break;
+        }
+        axl_setPosition(targ);
+        break;
+      default:
+        OSAP::error("we can only 'set' position, others are targs");
+        break;
+    }
+  } else {
+    switch(mode){
+      // case AXL_MODE_ACCEL:
+      //   // axl_setAccelTarget(targ);
+      //   break;
+      case AXL_MODE_VELOCITY:
+        axl_setVelocityTarget(targ);
+        break;
+      case AXL_MODE_POSITION:
+        axl_setPositionTarget(targ);
+        break;
+      default:
+        OSAP::error("AXL state targ has bad / unrecognized mode " + String(mode));
+        break;
+    }
+  }
+  // since we routinely update it w/ actual states (not requests) 
+  return EP_ONDATA_REJECT;
+}
+
+uint8_t stateDataDummy[256];
+
+boolean beforeAxlStateQuery(void);
+
+Endpoint stateEP(&osap, "axlState", onStateData, beforeAxlStateQuery);
+
+boolean beforeAxlStateQuery(void){
+  uint16_t len = axl_getState(stateDataDummy);
+  stateEP.write(stateDataDummy, len);
+  return true;
+}
+
+// -------------------------------------------------------- 4: Axl Queue Addition 
+
+EP_ONDATA_RESPONSES onSegmentData(uint8_t* data, uint16_t len){
+  // careful, if you add a new field in axlPlannedSegment_t, recall you have to copy 
+  // it manually into the buffer (!) 
+  axlPlannedSegment_t segment;
+  uint16_t rptr = 0;
+  // location of segment-in-sequence, to count continuity, 
+  segment.segmentNumber = ts_readUint32(data, &rptr);
+  // which actuator is requested to ack this mfer, 
+  segment.returnActuator = ts_readUint8(data, &rptr);
+  // is it the end of this stream ?
+  segment.isLastSegment = ts_readBoolean(data, &rptr);
+  OSAP::debug("segnum, isLast " + String(segment.segmentNumber) + ", " + String(segment.isLastSegment));
+  // unit vector describing segment's direction, 
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    segment.unitVector.axis[a] = ts_readFloat32(data, &rptr);
+  }
+  // start vel, accel-rate (up, and down), max velocity, final velocity, distance (all +ve)
+  segment.vi = ts_readFloat32(data, &rptr);
+  segment.accel = ts_readFloat32(data, &rptr);
+  segment.vmax = ts_readFloat32(data, &rptr);
+  segment.vf = ts_readFloat32(data, &rptr);
+  segment.distance = ts_readFloat32(data, &rptr);
+  // and send it... 
+  axl_addSegmentToQueue(segment);
+  // don't write to endpoint... 
+  return EP_ONDATA_REJECT;
+}
+
+Endpoint precalculatedSegmentEP(&osap, "segmentsIn", onSegmentData);
+
+// -------------------------------------------------------- 5: Halt Input 
+
+EP_ONDATA_RESPONSES onHaltInData(uint8_t* data, uint16_t len){
+  axl_halt(data[0]);
+  return EP_ONDATA_REJECT;
+}
+
+Endpoint haltInEP(&osap, "haltIn", onHaltInData);
+
+// -------------------------------------------------------- 6, 7, 8: Outputs
+
+Endpoint haltOutEP(&osap, "haltOut");
+Endpoint segmentAckOutEP(&osap, "segmentAckOut");
+Endpoint segmentCompleteOutEP(&osap, "segmentCompleteOut");
+
+// -------------------------------------------------------- 9: Motor Settings
+
+uint8_t axisPick = 0;
+boolean invert = false; 
+uint16_t microstep = 4; 
+float spu = 100.0F;
+float cscale = 0.1F;
+
+// aye, there should be a void onData overload... less confusing 
+EP_ONDATA_RESPONSES onMotorSettingsData(uint8_t* data, uint16_t len){
+  uint16_t rptr = 0;
+  axisPick = data[rptr ++];
+  ts_readBoolean(&invert, data, &rptr);
+  ts_readUint16(&microstep, data, &rptr);
+  spu = ts_readFloat32(data, &rptr);
+  cscale = ts_readFloat32(data, &rptr);
+  stepper_hw->setMicrostep(microstep);
+  stepper_hw->setCurrent(cscale);
+  stepper_hw->setInversion(invert);
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint motorSettingsEP(&osap, "motorSettings", onMotorSettingsData);
+
+// -------------------------------------------------------- 10: Limit Halt-Output:
+
+Endpoint limitHaltEP(&osap, "limitSwitchState");
+
+#define LIMIT_PIN 23
+#define LIMIT_PORT 0 
+
+void limitSetup(void){
+  PORT->Group[LIMIT_PORT].DIRCLR.reg = (1 << LIMIT_PIN);
+  PORT->Group[LIMIT_PORT].PINCFG[LIMIT_PIN].bit.INEN = 1;
+  // pullup 
+  PORT->Group[LIMIT_PORT].OUTSET.reg = (1 << LIMIT_PIN);
+}
+
+boolean checkLimit(void){
+  return (PORT->Group[LIMIT_PORT].IN.reg & (1 << LIMIT_PIN));
+}
+
+// -------------------------------------------------------- 11: Motion State 
+
+boolean beforeMotionStateQuery(void);
+
+Endpoint motionStateEP(&osap, "motionState", beforeMotionStateQuery);
+
+uint8_t dummyMotionStateData[1];
+
+boolean beforeMotionStateQuery(void){
+  if(axl_isMoving()){
+    dummyMotionStateData[0] = 1;
+  } else {
+    dummyMotionStateData[0] = 0;
+  }
+  motionStateEP.write(dummyMotionStateData, 1);
+  return true;
+}
+
+// -------------------------------------------------------- Arduino Setup 
+
+void setup() {
+  CLKLIGHT_SETUP;
+  ERRLIGHT_SETUP;
+  DEBUG1PIN_SETUP;
+  DEBUG2PIN_SETUP;
+  // port begin 
+  vpUSBSerial.begin();
+  vbUCBusDrop.begin();
+  // setup stepper machine 
+  stepper_hw->init(false, 0.0F);
+  stepper_hw->setMicrostep(4);
+  // setup controller
+  axl_setup();
+  // setup limit swootch
+  limitSetup();
+  // ticker begin:
+  // d51ClockUtils->start_ticker_a(AXL_TICKER_INTERVAL_US);
+}
+
+// -------------------------------------------------------- Das Loop 
+
+uint32_t lastBlink = 0;
+uint32_t blinkInterval = 50; // ms 
+
+uint8_t axlData[256];
+uint16_t axlDataLen = 0;
+
+uint32_t lastLimitCheck = 0;
+uint32_t limitCheckInterval = 50; // us, helps to debounce, bummer to be running this often 
+uint8_t limitTrace = 0; // 8-wide 1-bit state trace... for edge-masking, 
+boolean limitState = false;
+uint8_t dummy[2] = { 0, 0 }; // lol, typed endpoints wanted ! 
+
+void loop() {
+  osap.loop();
+  // check for halt info... 
+  axlDataLen = axl_getHaltPacket(axlData);
+  if(axlDataLen){
+    haltOutEP.write(axlData, axlDataLen);
+  }
+  // check for queueAck info... 
+  axlDataLen = axl_getSegmentAckMsg(axlData);
+  if(axlDataLen){
+    segmentAckOutEP.write(axlData, axlDataLen);
+  }
+  // check for queueSegmentComplete 
+  axlDataLen = axl_getSegmentCompleteMsg(axlData);
+  if(axlDataLen){
+    segmentCompleteOutEP.write(axlData, axlDataLen);
+  }
+  // refresh stepper hw, 
+  stepper_hw->dacRefresh();
+  if(lastBlink + blinkInterval < millis()){
+    lastBlink = millis();
+    CLKLIGHT_TOGGLE;
+    // updateStatesEP();
+    //axl_printHomeState();
+  }
+  // this, i.e, could be on an endpoint's loop code, non?
+  if(checkLimit() && limitState == false){
+    ERRLIGHT_ON;
+    limitState = true;
+    dummy[0] = 1;
+    limitHaltEP.write(dummy, 1);
+  } else if (!checkLimit() && limitState == true){
+    ERRLIGHT_OFF;
+    limitState = false;
+    dummy[0] = 0;
+    limitHaltEP.write(dummy, 1);
+  }
+  // if(lastLimitCheck + limitCheckInterval < micros()){
+  //   lastLimitCheck = micros();
+  //   // shift left one & tack bit on the end, 
+  //   limitTrace = limitTrace << 1;
+  //   limitTrace |= checkLimit() ? 1 : 0;
+  //   // swap on positive or -ve edges, 
+  //   if(limitTrace == 0b11111111 && limitState == false){
+  //     ERRLIGHT_ON;
+  //     limitState = true;
+  //     dummy[0] = 1;
+  //     limitHaltEP.write(dummy, 1);
+  //   } else if (limitTrace == 0b00000000 && limitState == true){
+  //     ERRLIGHT_OFF;
+  //     limitState = false;
+  //     dummy[0] = 0;
+  //     limitHaltEP.write(dummy, 1);
+  //   }
+  // }
+  // if(errLightOn && errLightOnTime + 250 < millis()){
+  //   ERRLIGHT_OFF;
+  //   errLightOn = false;
+  // }
+}
+
+// -------------------------------------------------------- Small-Time Ops 
+
+volatile float stepRatchet = 0.0F;
+void axl_onPositionDelta(uint8_t axis, float delta, float absolute){
+  if(axis != axisPick) return;
+  stepRatchet += delta * spu;
+  if(stepRatchet >= 1.0F){
+    stepper_hw->dir(true);
+    stepper_hw->step();
+    stepRatchet -= 1.0F;
+  } else if (stepRatchet <= -1.0F){
+    stepper_hw->dir(false);
+    stepper_hw->step();
+    stepRatchet += 1.0F;
+  }
+}
+
+// void TC0_Handler(void){
+//   DEBUG1PIN_ON;
+//   TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+//   TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+//   // run the loop, 
+//   axl_integrator();
+//   DEBUG1PIN_OFF;
+// }
+
+void ucBusDrop_onRxISR(void){
+  DEBUG1PIN_ON;
+  axl_integrator();
+  DEBUG1PIN_OFF;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osap_config.h b/system/firmware/lpf-axl-stepper/src/osap_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..f94ddc11991022908e22357c21e15e17a03fd82f
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osap_config.h
@@ -0,0 +1,34 @@
+/*
+osap_config.h
+
+config options for an osap-embedded build 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_CONFIG_H_
+#define OSAP_CONFIG_H_
+
+// size of vertex stacks, lenght, then count,
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 3  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+#define VT_MAXITEMSPERTURN 8
+
+// max # of endpoints that could be spawned here,
+#define MAX_CONTEXT_ENDPOINTS 64
+
+// count of routes each endpoint can have, 
+#define ENDPOINT_MAX_ROUTES 4
+#define ENDPOINT_ROUTE_MAX_LEN 64 
+
+// count of broadcast channels width, 
+#define VBUS_MAX_BROADCAST_CHANNELS 64 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/LICENSE.md b/system/firmware/lpf-axl-stepper/src/osape/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/README.md b/system/firmware/lpf-axl-stepper/src/osape/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c94ebaff92a9980dbc93aa25047846ee4aa64e0
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/README.md
@@ -0,0 +1,5 @@
+## OSAP Embedded 
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/loop.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/loop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c050974467d2fc95677d72f2e2da3b6608a0f588
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/loop.cpp
@@ -0,0 +1,255 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "loop.h"
+#include "packets.h"
+#include "osap.h"
+
+#define MAX_ITEMS_PER_LOOP 32
+//#define LOOP_DEBUG
+
+// we'll stack up to 64 messages to handle per loop, 
+// more items would cause issues: will throw errors and design circular looping at that point 
+stackItem* itemList[MAX_ITEMS_PER_LOOP];
+uint16_t itemListLen = 0;
+
+void listSetupRecursor(Vertex* vt){
+  // run the vertex' loop... but not if it's the root, yar 
+  if(vt->type != VT_TYPE_ROOT) vt->loop();
+  // for each input / output stack, try to collect all items... 
+  // alright I'm doing this collect... but want a kind of pickup-where-you-left-off thing, 
+  // so that we can have a fixed-length loop, i.e. 64 items per, but still do fairness... 
+  // otherwise our itemList has to be large enough to carry potentially every single item ? 
+  for(uint8_t od = 0; od < 2; od ++){
+    uint8_t count = stackGetItems(vt, od, &(itemList[itemListLen]), MAX_ITEMS_PER_LOOP - itemListLen);
+    itemListLen += count;
+  }
+  // recurse children...
+  for(uint8_t c = 0; c < vt->numChildren; c ++){
+    listSetupRecursor(vt->children[c]);
+  }
+}
+
+// sort-in-place based on time-to-death, 
+void listSort(stackItem** list, uint16_t listLen){
+  // write each item's time-to-death, 
+  uint32_t now = millis();
+  for(uint16_t i = 0; i < listLen; i ++){
+    list[i]->timeToDeath = ts_readUint16(list[i]->data, 0) - (now - list[i]->arrivalTime);
+  }
+  // also... vertex arrivalTime should be uint32_t milliseconds of arrival... 
+  #warning not-yet sorted... 
+}
+
+// this handles internal transport... checking for errors along paths, and running flowcontrol 
+// returns true to wipe current item, false to leave-in-wait, 
+boolean internalTransport(stackItem* item, uint16_t ptr){
+  // we walk thru our little internal tree here, 
+  Vertex* vt = item->vt;
+  // ptr for the walk, use item->data[ptr] == PK_INSTRUCTION, not PK_PTR, 
+  uint16_t fwdPtr = ptr + 1;
+  // count # of ops, 
+  uint8_t opCount = 0;
+  // for a max. of 16 fwd steps, 
+  for(uint8_t s = 0; s < 16; s ++){
+    uint16_t arg = readArg(item->data, fwdPtr);
+    switch(PK_READKEY(item->data[fwdPtr])){
+      // ---------------------------------------- Internal Dir Cases 
+      case PK_SIB:
+        // check validity of route & shift our reference vt,
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during sib transport"); return true;
+        } else if (arg >= vt->parent->numChildren){
+          OSAP::error("no sibling " + String(arg) + " at " + vt->name + " during sib transport"); return true;
+        } else {
+          // this is it: we go fwds to this vt & end-of-switch statements increment ptrs
+          vt = vt->parent->children[arg];
+        }
+        break;
+      case PK_PARENT:
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during parent transport"); return true;
+        } else {
+          // likewise... 
+          vt = vt->parent;
+        }
+        break;
+      case PK_CHILD:
+        if(arg >= vt->numChildren){
+          OSAP::error("no child " + String(arg) + " at " + vt->name + " during child transport"); return true;
+        } else {
+          // again, just walk fwds... 
+          vt = vt->children[arg];
+        }
+        break;
+      // ---------------------------------------- Terminal / Exit Cases 
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD: 
+      case PK_DEST:
+      case PK_PINGREQ:
+      case PK_SCOPEREQ:
+      case PK_LLESCAPE:
+        // check / transport...
+        if(stackEmptySlot(vt, VT_STACK_DESTINATION)){
+          // walk the ptr fwds, 
+          walkPtr(item->data, item->vt, opCount, ptr);
+          // ingest at the new place, 
+          stackLoadSlot(vt, VT_STACK_DESTINATION, item->data, item->len);
+          // return true to clear it out, 
+          return true;
+        } else {
+          return false; 
+        }
+      default:
+        OSAP::error("internal transport failure, ptr walk ends at unknown key");
+        return true;
+    } // end switch 
+    fwdPtr += 2;
+    opCount ++;
+  } // end max-16-steps, 
+  // if we're past all 16 and didn't hit a terminal, pckt is eggregiously long, rm it 
+  return true;
+}
+
+// -------------------------------------------------------- LOOP Begins Here 
+
+// ... would be breadth-first, ideally 
+void osapLoop(Vertex* root){
+  // we want to build a list of items, recursing through... 
+  itemListLen = 0;
+  listSetupRecursor(root);
+  // check now if items are nearly oversized...
+  // see notes in the log from 2022-06-22 if this error occurs, 
+  if(itemListLen >= MAX_ITEMS_PER_LOOP - 2){
+    OSAP::error("loop items exceeds " + String(MAX_ITEMS_PER_LOOP) + ", breaking per-loop transport properties... pls fix", HALTING);
+  }
+  // stash high-water mark,
+  if(itemListLen > OSAP::loopItemsHighWaterMark) OSAP::loopItemsHighWaterMark = itemListLen;
+  // log 'em 
+  // OSAP::debug("list has " + String(itemListLen) + " elements", LOOP);
+  // otherwise we can carry on... the item should be sorted, global vars, 
+  listSort(itemList, itemListLen);
+  // then we can handle 'em one by one 
+  for(uint16_t i = 0; i < itemListLen; i ++){
+    osapItemHandler(itemList[i]);
+  }
+}
+
+void osapItemHandler(stackItem* item){
+  // clear dead items, 
+  if(item->timeToDeath < 0){
+    OSAP::debug(  "item at " + item->vt->name + " times out w/ " + String(item->timeToDeath) + 
+                  " ms to live, of " + String(ts_readUint16(item->data, 0)) + " ttl", LOOP);
+    stackClearSlot(item);
+    return;
+  }
+  // get a ptr for the item, 
+  uint16_t ptr = 0;
+  if(!findPtr(item->data, &ptr)){    
+    OSAP::error("item at " + item->vt->name + " unable to find ptr, deleting...");
+    stackClearSlot(item);
+    return;
+  }
+  // now the handle-switch, item->data[ptr] = PK_PTR, we switch on instruction which is behind that, 
+  switch(PK_READKEY(item->data[ptr + 1])){
+    // ------------------------------------------ Terminal / Destination Switches 
+    case PK_DEST:
+      item->vt->destHandler(item, ptr);
+      break;
+    case PK_PINGREQ:
+      item->vt->pingRequestHandler(item, ptr);
+      break;
+    case PK_SCOPEREQ:
+      item->vt->scopeRequestHandler(item, ptr);
+      break;
+    case PK_PINGRES:
+    case PK_SCOPERES:
+      OSAP::error("ping or scope request issued to " + item->vt->name + " not handling those in embedded", MEDIUM);
+      stackClearSlot(item);
+      break;
+    // ------------------------------------------ Internal Transport 
+    case PK_SIB:
+    case PK_PARENT:
+    case PK_CHILD:  // transport handler returns true if msg should be wiped, false if it should be cycled
+      if(internalTransport(item, ptr)){
+        stackClearSlot(item);
+      }
+      break;
+    // ------------------------------------------ Network Transport 
+    case PK_PFWD:
+      // port forward...
+      if(item->vt->vport == nullptr){
+        OSAP::error("pfwd to non-vport " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        if(item->vt->vport->cts()){
+          // walk one step, but only if fn returns true (having success) 
+          if(walkPtr(item->data, item->vt, 1, ptr)) item->vt->vport->send(item->data, item->len);
+          stackClearSlot(item);
+        } else {
+          // failed to send this turn (flow controlled), will return here next round 
+        }
+      }
+      break;
+    case PK_BFWD:
+    case PK_BBRD:
+      // bus forward / bus broadcast: 
+      if(item->vt->vbus == nullptr){
+        OSAP::error("bfwd to non-vbus " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        // arg is rxAddr for bus-forwards, is broadcastChannel for bus-broadcast, 
+        uint16_t arg = readArg(item->data, ptr + 1);
+        if(item->data[ptr + 1] == PK_BFWD){
+          if(item->vt->vbus->cts(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              item->vt->vbus->send(item->data, item->len, arg);
+            } else {
+              OSAP::error("bfwd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bfwd (flow controlled), returning here next round... 
+          }
+        } else if (item->data[ptr + 1] == PK_BBRD){
+          if(item->vt->vbus->ctb(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              // OSAP::debug("broadcasting on ch " + String(arg));
+              item->vt->vbus->broadcast(item->data, item->len, arg);
+            } else {
+              OSAP::error("bbrd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bbrd, returning next... 
+          }
+        } else {
+          // doesn't make any sense, we switched in on these terms... 
+          OSAP::error("absolute nonsense", MEDIUM);
+          stackClearSlot(item);
+        }
+      }
+      break;
+    case PK_LLESCAPE:
+      OSAP::error("lldebug to embedded, dumping", MINOR);
+      stackClearSlot(item);
+      break;
+    default:
+      OSAP::error("unrecognized ptr to " + item->vt->name + " " + String(PK_READKEY(item->data[ptr + 1])), MINOR);
+      stackClearSlot(item);
+      // error, delete, 
+      break;
+  } // end swiiiitch 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/loop.h b/system/firmware/lpf-axl-stepper/src/osape/core/loop.h
new file mode 100644
index 0000000000000000000000000000000000000000..5022aa16c00da6b40864ca8f09432dab0744ad04
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/loop.h
@@ -0,0 +1,25 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef LOOP_H_
+#define LOOP_H_ 
+
+#include "vertex.h"
+
+// we loop, 
+void osapLoop(Vertex* root);
+// we handle, 
+void osapItemHandler(stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/osap.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/osap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acde43271ecb27ea482e1b1079b02d847a15fed9
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/osap.cpp
@@ -0,0 +1,111 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "loop.h"
+#include "packets.h"
+#include "../utils/cobs.h"
+
+// stash most recents, and counts, and high water mark, 
+uint32_t OSAP::loopItemsHighWaterMark = 0;
+uint32_t errorCount = 0;
+uint32_t debugCount = 0;
+// strings...
+unsigned char latestError[VT_SLOTSIZE];
+unsigned char latestDebug[VT_SLOTSIZE];
+uint16_t latestErrorLen = 0;
+uint16_t latestDebugLen = 0;
+
+OSAP::OSAP(String _name) : Vertex("rt_" + _name){};
+
+void OSAP::loop(void){
+  // this is the root, so we kick all of the internal net operation from here 
+  osapLoop(this);
+}
+
+void OSAP::destHandler(stackItem* item, uint16_t ptr){
+  // classic switch on 'em 
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == ROOT_KEY, ptr + 3 = ID (if ack req.) 
+  uint16_t wptr = 0;
+  uint16_t len = 0;
+  switch(item->data[ptr + 2]){
+    case RT_DBG_STAT:
+    case RT_DBG_ERRMSG:
+    case RT_DBG_DBGMSG:
+      // return w/ the res key & same issuing ID 
+      payload[wptr ++] = PK_DEST;
+      payload[wptr ++] = RT_DBG_RES;
+      payload[wptr ++] = item->data[ptr + 3];
+      // stash high water mark, errormsg count, debugmsgcount 
+      ts_writeUint32(OSAP::loopItemsHighWaterMark, payload, &wptr);
+      ts_writeUint32(errorCount, payload, &wptr);
+      ts_writeUint32(debugCount, payload, &wptr);
+      // optionally, a string... I know we switch() then if(), it's uggo, 
+      if(item->data[ptr + 2] == RT_DBG_ERRMSG){
+        ts_writeString(latestError, latestErrorLen, payload, &wptr, VT_SLOTSIZE / 2);
+      } else if (item->data[ptr + 2] == RT_DBG_DBGMSG){
+        ts_writeString(latestDebug, latestDebugLen, payload, &wptr, VT_SLOTSIZE / 2);
+      }
+      // that's the payload, I figure, 
+      len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+      stackClearSlot(item);
+      stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      break;
+    default:
+      OSAP::error("unrecognized key to root node " + String(item->data[ptr + 2]));
+      stackClearSlot(item);
+      break;
+  }
+}
+
+uint8_t errBuf[255];
+uint8_t errBufEncoded[255];
+
+void debugPrint(String msg){
+  // whatever you want,
+  uint32_t len = msg.length();
+  // max this long, per the serlink bounds 
+  if(len + 9 > 255) len = 255 - 9;
+  // header... 
+  errBuf[0] = len + 8;  // len, key, cobs start + end, strlen (4) 
+  errBuf[1] = 172;      // serialLink debug key 
+  errBuf[2] = len & 255;
+  errBuf[3] = (len >> 8) & 255;
+  errBuf[4] = (len >> 16) & 255;
+  errBuf[5] = (len >> 24) & 255;
+  msg.getBytes(&(errBuf[6]), len + 1);
+  // encode from 2, leaving the len, key header... 
+  size_t ecl = cobsEncode(&(errBuf[2]), len + 4, errBufEncoded);
+  // what in god blazes ? copy back from encoded -> previous... 
+  memcpy(&(errBuf[2]), errBufEncoded, ecl);
+  // set tail to zero, to delineate, 
+  errBuf[errBuf[0] - 1] = 0;
+  // direct escape 
+  Serial.write(errBuf, errBuf[0]);
+}
+
+void OSAP::error(String msg, OSAPErrorLevels lvl){
+  //const char* str = msg.c_str();
+  msg.getBytes(latestError, VT_SLOTSIZE);
+  latestErrorLen = msg.length();
+  errorCount ++;
+  debugPrint(msg);
+}
+
+void OSAP::debug(String msg, OSAPDebugStreams stream){
+  msg.getBytes(latestDebug, VT_SLOTSIZE);
+  latestDebugLen = msg.length();
+  debugCount ++;
+  debugPrint(msg);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/osap.h b/system/firmware/lpf-axl-stepper/src/osape/core/osap.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c2c9d789ebd23ba452c7259c3423088ff2b9f
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/osap.h
@@ -0,0 +1,38 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+
+// largely semantic class, OSAP represents the root vertex in whichever context 
+// and it's where run the main loop from, etc... 
+// here is where we coordinate context-level stuff: adding new instances, 
+// stashing error messages & counts, etc, 
+
+enum OSAPErrorLevels { HALTING, MEDIUM, MINOR };
+enum OSAPDebugStreams { DEFAULT, LOOP };
+
+class OSAP : public Vertex {
+  public: 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr);
+    OSAP(String _name);// : Vertex(_name);
+    static void error(String msg, OSAPErrorLevels lvl = MINOR );
+    static void debug(String msg, OSAPDebugStreams stream = DEFAULT );
+    static uint32_t loopItemsHighWaterMark;
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/packets.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/packets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf83928d99d3c173d0efdef40ab614dc2433b409
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/packets.cpp
@@ -0,0 +1,193 @@
+/*
+osap/packets.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "packets.h"
+#include "ts.h"
+#include "osap.h"
+
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg){
+  buf[ptr] = key | (0b00001111 & (arg >> 8));
+  buf[ptr + 1] = arg & 0b11111111;
+}
+// not sure how I want to do this yet... 
+uint16_t readArg(uint8_t* buf, uint16_t ptr){
+  return ((buf[ptr] & 0b00001111) << 8) | buf[ptr + 1];
+}
+
+boolean findPtr(uint8_t* pck, uint16_t* pt){
+  // 1st instruction is always at pck[4], pck[0][1] == ttl, pck[2][3] == segSize 
+  uint16_t ptr = 4;
+  // there's a potential speedup where we assume given *pt is already incremented somewhat, 
+  // maybe shaves some ns... but here we just look fresh every time, 
+  for(uint8_t i = 0; i < 16; i ++){
+    switch(PK_READKEY(pck[ptr])){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        ptr += 2;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+boolean walkPtr(uint8_t* pck, Vertex* source, uint8_t steps, uint16_t ptr){
+  // if the ptr we were handed isn't in the right spot, try to find it... 
+  if(pck[ptr] != PK_PTR){
+    // if that fails, bail... 
+    if(!findPtr(pck, &ptr)){
+      OSAP::error("before a ptr walk, ptr is out of place...");
+      return false;
+    }
+  }
+  // carry on w/ the walking algo, 
+  for(uint8_t s = 0; s < steps; s ++){
+    switch PK_READKEY(pck[ptr + 1]){
+      case PK_SIB:
+        {
+          // stash indice from-whence it came,
+          uint16_t txIndice = source->indice;
+          // for loop's next step, this is the source now, 
+          source = source->parent->children[readArg(pck, ptr + 1)];
+          // where ptr is currently, we stash new key/pair for a reversal, 
+          writeKeyArgPair(pck, ptr, PK_SIB, txIndice);
+          // increment packet's ptr, and our own... 
+          pck[ptr + 2] = PK_PTR; 
+          ptr += 2;
+        }
+        break;
+      case PK_PARENT:
+        // reversal for a 'parent' instruction is to bounce back down to the child, 
+        writeKeyArgPair(pck, ptr, PK_CHILD, source->indice);
+        // next source is now...
+        source = source->parent;
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      case PK_CHILD:
+        // next source is... 
+        source = source->children[readArg(pck, ptr + 1)];
+        // reversal for 'child' instruction is to go back up to parent, 
+        writeKeyArgPair(pck, ptr, PK_PARENT, 0);
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2; 
+        break;
+      case PK_PFWD:
+        // reversal for pfwd instruction is identical, 
+        writeKeyArgPair(pck, ptr, PK_PFWD, 0);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // though this should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have port fwd w/ more than one step");
+          return false;
+        }
+        break;
+      case PK_BFWD:
+        // reversal for bfwd instruction is to return *up*... 
+        writeKeyArgPair(pck, ptr, PK_BFWD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // this also should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have bus fwd w/ more than one step");
+          return false; 
+        }
+        break;
+      case PK_BBRD:
+        // broadcasts are a little strange, we also stuff the ownRxAddr in,
+        writeKeyArgPair(pck, ptr, PK_BBRD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      default:
+        OSAP::error("have out of place keys in the ptr walk...");
+        return false;
+    }
+  } // end steps, alleged success,  
+  return true; 
+}
+
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen){
+  uint16_t wptr = 0;
+  ts_writeUint16(route->ttl, gram, &wptr);
+  ts_writeUint16(route->segSize, gram, &wptr);
+  memcpy(&(gram[wptr]), route->path, route->pathLen);
+  wptr += route->pathLen;
+  if(wptr + payloadLen > route->segSize){
+    OSAP::error("writeDatagram asked to write packet that exceeds segSize, bailing", MEDIUM);
+    return 0;
+  }
+  memcpy(&(gram[wptr]), payload, payloadLen);
+  wptr += payloadLen;
+  return wptr;
+}
+
+// original gram, payload, len, 
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen){
+  // 1st up, we can straight copy the 1st 4 bytes, 
+  memcpy(gram, ogGram, 4);
+  // now find a ptr, 
+  uint16_t ptr = 0;
+  if(!findPtr(ogGram, &ptr)){
+    OSAP::error("writeReply can't find the pointer...", MEDIUM);
+    return 0;
+  }
+  // do we have enough space? it's the minimum of the allowed segsize & stated maxGramLength, 
+  maxGramLength = min(maxGramLength, ts_readUint16(ogGram, 2));
+  if(ptr + 1 + payloadLen > maxGramLength){
+    OSAP::error("writeReply asked to write packet that exceeds maxGramLength, bailing", MEDIUM);
+    return 0;
+  }
+  // write the payload in, apres-pointer, 
+  memcpy(&(gram[ptr + 1]), payload, payloadLen);
+  // now we can do a little reversing... 
+  uint16_t wptr = 4;
+  uint16_t end = ptr;
+  uint16_t rptr = ptr;
+  // 1st byte... the ptr, 
+  gram[wptr ++] = PK_PTR;
+  // now for a max 16 steps, 
+  for(uint8_t h = 0; h < 16; h ++){
+    if(wptr >= end) break;
+    rptr -= 2;
+    switch(PK_READKEY(ogGram[rptr])){
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        gram[wptr ++] = ogGram[rptr];
+        gram[wptr ++] = ogGram[rptr + 1];
+        break;
+      default:
+        OSAP::error("writeReply fails to reverse this packet, bailing", MEDIUM);
+        return 0;
+    }
+  } // end thru-loop, 
+  // it's written, return the len  // we had gram[ptr] = PK_PTR, so len was ptr + 1, then added payloadLen, 
+  return end + 1 + payloadLen;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/packets.h b/system/firmware/lpf-axl-stepper/src/osape/core/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..914656be1eb7656f481915438a10701edad23280
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/packets.h
@@ -0,0 +1,48 @@
+/*
+osap/packets.h
+
+reading / writing from osap packets / datagrams 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_PACKETS_H_
+#define OSAP_PACKETS_H_
+
+#include <Arduino.h>
+#include "vertex.h"
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 240
+#define PK_DEST 224
+#define PK_PINGREQ 192 
+#define PK_PINGRES 176 
+#define PK_SCOPEREQ 160 
+#define PK_SCOPERES 144 
+#define PK_SIB 16 
+#define PK_PARENT 32 
+#define PK_CHILD 48 
+#define PK_PFWD 64 
+#define PK_BFWD 80
+#define PK_BBRD 96 
+#define PK_LLESCAPE 112 
+
+// to read *just the key* from key, arg pair
+#define PK_READKEY(data) (data & 0b11110000)
+
+// packet utes, 
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg);
+uint16_t readArg(uint8_t* buf, uint16_t ptr);
+boolean findPtr(uint8_t* pck, uint16_t* ptr);
+boolean walkPtr(uint8_t* pck, Vertex* vt, uint8_t steps, uint16_t ptr = 4);
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen);
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/routes.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/routes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6caea0c0a00c56f339f2fdb7ec4b02278e1faf73
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/routes.cpp
@@ -0,0 +1,55 @@
+/*
+osap/routes.cpp
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "routes.h"
+#include "packets.h"
+
+Route::Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize){
+  ttl = _ttl;
+  segSize = _segSize;
+  // nope, 
+  if(_pathLen > 64){
+    _pathLen = 0;
+  }
+  memcpy(path, _path, _pathLen);
+  pathLen = _pathLen;
+}
+
+Route::Route(void){
+  path[pathLen ++] = PK_PTR;
+}
+
+Route* Route::sib(uint16_t indice){
+  writeKeyArgPair(path, pathLen, PK_SIB, indice);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::pfwd(void){
+  writeKeyArgPair(path, pathLen, PK_PFWD, 0);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bfwd(uint16_t rxAddr){
+  writeKeyArgPair(path, pathLen, PK_BFWD, rxAddr);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bbrd(uint16_t channel){
+  writeKeyArgPair(path, pathLen, PK_BBRD, channel);
+  pathLen += 2;
+  return this; 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/routes.h b/system/firmware/lpf-axl-stepper/src/osape/core/routes.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2bb3c97cffb7df24867de4efe7489b40daa4a0e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/routes.h
@@ -0,0 +1,38 @@
+/*
+osap/routes.h
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_ROUTES_H_
+#define OSAP_ROUTES_H_
+
+#include <Arduino.h>
+
+// a route type... 
+class Route {
+  public:
+    uint8_t path[64];
+    uint16_t pathLen = 0;
+    uint16_t ttl = 1000;
+    uint16_t segSize = 128;
+    // write-direct constructor, 
+    Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize);
+    // write-along constructor, 
+    Route(void);
+    // pass-thru initialize constructors, 
+    Route* sib(uint16_t indice);
+    Route* pfwd(void);
+    Route* bfwd(uint16_t rxAddr);
+    Route* bbrd(uint16_t channel);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/stack.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/stack.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..401bd7103f872bd141172d945ff2b2a8cb93e36f
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/stack.cpp
@@ -0,0 +1,138 @@
+/*
+osap/stack.cpp
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "stack.h"
+#include "vertex.h"
+#include "osap.h"
+
+// ---------------------------------------------- Stack Tools 
+
+void stackReset(Vertex* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+      // and ptrs to self, 
+      vt->stack[od][s].vt = vt;
+      vt->stack[od][s].od = od;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(Vertex* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //DEBUG("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item){
+  // this would be deadly, so:
+  if(od > 1) {
+    OSAP::error("stackClearSlot, od > 1, badness", MEDIUM);
+    return;
+  }
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+  }
+  // now we callback to the vertex; these fns are often used to clear flowcontrol condns 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      vt->onDestinationStackClear(indice);
+      break;
+    default:  // guarded against this above... 
+      break;
+  }
+}
+
+void stackClearSlot(stackItem* item){
+  stackClearSlot(item->vt, item->od, item);
+}
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/stack.h b/system/firmware/lpf-axl-stepper/src/osape/core/stack.h
new file mode 100644
index 0000000000000000000000000000000000000000..79151239b987f025150dc6f1ac580cfc4e474887
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/stack.h
@@ -0,0 +1,54 @@
+/*
+osap/stack.h
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STACK_H_
+#define STACK_H_ 
+
+#include <Arduino.h>
+#include "./osap_config.h" 
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+class Vertex;
+
+// core routing layer chunk-of-stuff, 
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+typedef struct stackItem {
+  uint8_t data[VT_SLOTSIZE];          // data bytes
+  uint16_t len = 0;                   // data bytes count 
+  uint32_t arrivalTime = 0;           // ms-since-system-alive, time at last ingest
+  int32_t timeToDeath = 0;            // ms of time until pckt vanishes on this hop
+  Vertex* vt;                         // vertex to whomst we belong, 
+  uint8_t od;                         // origin / destination to which we belong, 
+  uint8_t indice;                     // actual physical position in the stack 
+  uint16_t ptr = 0;                   // current data[ptr] == 88 
+  stackItem* next = nullptr;          // linked ringbuffer next 
+  stackItem* previous = nullptr;      // linked ringbuffer previous 
+} stackItem;
+
+// stack setup / reset 
+void stackReset(Vertex* vt);
+
+// stack origin side 
+boolean stackEmptySlot(Vertex* vt, uint8_t od);
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// stack exit side 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item);
+void stackClearSlot(stackItem* item);
+
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/ts.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/ts.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cd3fdc9c1c249d25b22b75fa9fc69f311d04c19
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/ts.cpp
@@ -0,0 +1,183 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+// ---------------------------------------------- Reading 
+
+// boolean 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr){
+  if(buf[(*ptr) ++]){
+    *val = true;
+  } else {
+    *val = false;
+  }
+}
+
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr){
+  boolean val = buf[(*ptr)] ? true : false;
+  (*ptr) += 1;
+  return val;
+}
+
+// uint8 
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr){
+  uint8_t val = buf[(*ptr)];
+  (*ptr) += 1;
+  return val;
+}
+
+// uint16 
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+#warning some of these are pretty vague, i.e. this ingests a pointer *not as a pointer* (lol)
+// so it doesn't increment it, whereas the readUint8 above *does so* - ... ?? pick a style ? 
+uint16_t ts_readUint16(unsigned char* buf, uint16_t ptr){
+  return (buf[ptr + 1] << 8) | buf[ptr];
+}
+
+// uint32 
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr){
+  uint32_t val = (buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)]);
+  (*ptr) += 4;
+  return val;
+}
+
+// int32 
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.i;
+}
+
+// float32 
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.f;
+}
+
+// -------------------------------------------------------- Writing 
+
+// boolean
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+// unsigned 
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+// signed 
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int16 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+}
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+// floats 
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+// string, overloaded ?
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen){
+  if(strLen > maxLen) strLen = maxLen;
+  buf[(*ptr) ++] = strLen & 255;
+  buf[(*ptr) ++] = (strLen >> 8) & 255;
+  buf[(*ptr) ++] = (strLen >> 16) & 255;
+  buf[(*ptr) ++] = (strLen >> 24) & 255;
+  // write in one-by-one, surely there is a better way, 
+  for(uint16_t i = 0; i < strLen; i ++){
+    buf[(*ptr) ++] = str[i];
+  }
+  *ptr += strLen;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/ts.h b/system/firmware/lpf-axl-stepper/src/osape/core/ts.h
new file mode 100644
index 0000000000000000000000000000000000000000..63e77b2b02c0f7716bb1cba55cc9eef613d1207f
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/ts.h
@@ -0,0 +1,157 @@
+/*
+core/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22       // top level 
+#define VT_TYPE_MODULE 23     // collection of things (?) or something, idk yet 
+#define VT_TYPE_ENDPOINT 24   // software endpoint w/ read/write semantics 
+#define VT_TYPE_QUERY 25 
+#define VT_TYPE_ENDPOINT_MULTISEG 26 // likewise, but requring multisegment transmission 
+#define VT_TYPE_CODE 25       // autonomous graph dwellers 
+#define VT_TYPE_VPORT 44      // virtual ports 
+#define VT_TYPE_VBUS 45       // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+#define EP_ROUTE_QUERY_REQ 141 
+#define EP_ROUTE_QUERY_RES 142
+#define EP_ROUTE_SET_REQ 143
+#define EP_ROUTE_SET_RES 144 
+#define EP_ROUTE_RM_REQ 147
+#define EP_ROUTE_RM_RES 148 
+
+#define EP_ROUTEMODE_ACKED 167
+#define EP_ROUTEMODE_ACKLESS 168 
+
+// -------------------------------------------------------- Root Keys 
+
+#define RT_DBG_STAT 151
+#define RT_DBG_ERRMSG 152 
+#define RT_DBG_DBGMSG 153
+#define RT_DBG_RES 161
+
+// -------------------------------------------------------- VBus MVC Keys 
+
+#define VBUS_BROADCAST_MAP_REQ 145
+#define VBUS_BROADCAST_MAP_RES 146
+#define VBUS_BROADCAST_QUERY_REQ 141
+#define VBUS_BROADCAST_QUERY_RES 142
+#define VBUS_BROADCAST_SET_REQ 143
+#define VBUS_BROADCAST_SET_RES 144 
+#define VBUS_BROADCAST_RM_REQ 147 
+#define VBUS_BROADCAST_RM_RES 148 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int16 {
+  uint8_t bytes[2];
+  int16_t i;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr);
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr);
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+uint16_t ts_readUint16(uint8_t* buf, uint16_t ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr);
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+// -------------------------------------------------------- Writing 
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/vertex.cpp b/system/firmware/lpf-axl-stepper/src/osape/core/vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9ce012af681a42b059c6585888d1db806dd2ab51
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/vertex.cpp
@@ -0,0 +1,327 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "stack.h"
+#include "osap.h"
+#include "packets.h"
+
+// ---------------------------------------------- Temporary Stash 
+
+uint8_t Vertex::payload[VT_SLOTSIZE];
+uint8_t Vertex::datagram[VT_SLOTSIZE];
+
+// ---------------------------------------------- Vertex Constructor and Defaults 
+
+Vertex::Vertex( 
+  Vertex* _parent, String _name, 
+  void (*_loop)(Vertex* vt),
+  void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+  void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+){
+  // name self, reset stack... 
+  name = _name;
+  stackReset(this);
+  // callback assignments... 
+  loop_cb = _loop;
+  onOriginStackClear_cb = _onOriginStackClear;
+  onDestinationStackClear_cb = _onDestinationStackClear;
+  // insert self to osap net,
+  if(_parent == nullptr){
+    type = VT_TYPE_ROOT;
+    indice = 0;
+  } else {
+    if (_parent->numChildren >= VT_MAXCHILDREN) {
+      OSAP::error("trying to nest a vertex under " + _parent->name + " but we have reached VT_MAXCHILDREN limit", HALTING);
+    } else {
+      this->indice = _parent->numChildren;
+      this->parent = _parent;
+      _parent->children[_parent->numChildren ++] = this;
+    }
+  }
+}
+
+void Vertex::loop(void){
+  if(loop_cb != nullptr) return loop_cb(this);
+}
+
+void Vertex::destHandler(stackItem* item, uint16_t ptr){
+  // generic handler...
+  OSAP::debug("generic destHandler at " + name);
+  stackClearSlot(item);
+}
+
+void Vertex::pingRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_PINGRES;
+  payload[1] = item->data[ptr + 2];
+  // write a new gram, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 2);
+  // clear previous, 
+  stackClearSlot(item);
+  // load next... there will be one empty, as this has just arrived here... & we just wiped it 
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+void Vertex::scopeRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_SCOPERES;
+  payload[1] = item->data[ptr + 2];
+  // next items write starting here, 
+  uint16_t wptr = 2;
+  // scope time-tag, 
+  ts_writeUint32(scopeTimeTag, payload, &wptr);
+  // and read in the previous scope (this is traversal state required to delineate loops in the graph) 
+  uint16_t rptr = ptr + 3;
+  ts_readUint32(&scopeTimeTag, item->data, &rptr);
+  // write the vertex type,  
+  payload[wptr ++] = type;
+  // vport / vbus link states, 
+  if(type == VT_TYPE_VPORT){
+    payload[wptr ++] = (vport->isOpen() ? 1 : 0);
+  } else if (type == VT_TYPE_VBUS){
+    uint16_t addrSize = vbus->addrSpaceSize;
+    uint16_t addr = 0;
+    // ok we write the address size in first, then our own rxaddr, 
+    ts_writeUint16(vbus->addrSpaceSize, payload, &wptr);
+    ts_writeUint16(vbus->ownRxAddr, payload, &wptr);
+    // then *so long a we're not overwriting*, we stuff link-state bytes, 
+    while(wptr + 8 + name.length() <= VT_SLOTSIZE){
+      payload[wptr] = 0;
+      for(uint8_t b = 0; b < 8; b ++){
+        payload[wptr] |= (vbus->isOpen(addr) ? 1 : 0) << b;
+        addr ++;
+        if(addr >= addrSize) goto end;
+      }
+      wptr ++;
+    }
+    end:
+    wptr ++; // += 1 more, so we write into next, 
+  }
+  // our own indice, # siblings, and # children, 
+  ts_writeUint16(indice, payload, &wptr);
+  if(parent != nullptr){
+    ts_writeUint16(parent->numChildren, payload, &wptr);
+  } else {
+    ts_writeUint16(0, payload, &wptr);
+  }
+  ts_writeUint16(numChildren, payload, &wptr);
+  // finally, our string name:
+  ts_writeString(name, payload, &wptr);
+  // and roll that back up, rm old, and ship it, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+  stackClearSlot(item);
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+
+void Vertex::onOriginStackClear(uint8_t slot){
+  if(onOriginStackClear_cb != nullptr) return onOriginStackClear_cb(this, slot);
+}
+
+void Vertex::onDestinationStackClear(uint8_t slot){
+  if(onDestinationStackClear_cb != nullptr) return onDestinationStackClear_cb(this, slot);
+}
+
+// ---------------------------------------------- VPort Constructor and Defaults 
+
+VPort::VPort(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vp_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VPORT;
+  vport = this; 
+}
+
+// ---------------------------------------------- VBus Constructor and Defaults 
+
+VBus::VBus(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vb_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VBUS;
+  vbus = this;
+  // these should all init to nullptr, 
+  for(uint8_t ch = 0; ch < VBUS_MAX_BROADCAST_CHANNELS; ch ++){
+    broadcastChannels[ch] = nullptr;
+  }
+}
+
+void VBus::injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  // ok so first we want to see if we have anything sub'd to this channel, so
+  if(broadcastChannels[broadcastChannel] != nullptr){
+    // we have a route, so we want to load this data *as we inject some new path segments* 
+    Route* route = broadcastChannels[broadcastChannel];
+    // we could definitely do this faster w/o using the stackLoadSlot fn, but we won't do that yet... 
+    // will use the vertex-global datagram stash for that 
+    uint16_t ptr = 0; 
+    if(!findPtr(data, &ptr)){ OSAP::error("can't find ptr during broadcast injest", MEDIUM); return; }
+    // packet should look like 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <payload>
+    // we want to inject the channel's route such that 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <ch_route>, <payload>
+    // shouldn't actually be too difficult, eh?
+    // we do need to guard on lengths, 
+    if(len + route->pathLen > VT_SLOTSIZE){ OSAP::error("datagram + channel route is too large", MEDIUM); return; }
+    // copy up to PTR: pck[ptr] == PK_PTR, so we want to *include* this byte, having len ptr + 1, 
+    memcpy(datagram, data, ptr + 1);
+    // copy in route, but recall that as initialized, route->path[0] == PK_PTR, we don't want to double that up, 
+    memcpy(&(datagram[ptr + 1]), &(route->path[1]), route->pathLen - 1);
+    // then the rest of the gram, from just after-the-ptr, to end, 
+    memcpy(&datagram[ptr + 1 + route->pathLen - 1], &(data[ptr + 1]), len - ptr - 1);
+    // now we can load this in, 
+    stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len + route->pathLen - 1);
+    // aye that's it innit? 
+  }
+}
+
+void VBus::setBroadcastChannel(uint8_t channel, Route* route){
+  if(channel >= VBUS_MAX_BROADCAST_CHANNELS) return;
+  // seems a little sus, idk 
+  broadcastChannels[channel] = route;
+}
+
+void VBus::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == the key we're switching on...
+  switch(item->data[ptr + 2]){
+    case VBUS_BROADCAST_MAP_REQ:
+      // mvc request a map of our active broadcast channels, this is akin to bus link-state-scope packet
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_MAP_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // max length of channels... max 255, same as max endpoint routes (?) 
+        // this is maybe an error, consult packet spec (transport layer) for completeness, 
+        // time being... rare to have > 255 broadcast channels, 
+        payload[wptr ++] = VBUS_MAX_BROADCAST_CHANNELS;
+        // then *so long a we're not overwriting*, we stuff link-state bytes, 
+        // idk, 32 is arbitrary, we have to account for return-route length properly... 
+        uint16_t channel = 0;
+        while(wptr + 32 <= VT_SLOTSIZE){
+          payload[wptr] = 0;
+          for(uint8_t b = 0; b < 8; b ++){
+            payload[wptr] |= (broadcastChannels[channel] == nullptr ? 0 : 1) << b;
+            channel ++;
+            if(channel >= VBUS_MAX_BROADCAST_CHANNELS) goto end;
+          }
+          wptr ++;
+        }
+        end:
+        wptr ++; // += 1 more, so we write into next, 
+        // we're ready to write the reply back, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_QUERY_REQ:
+      // mvc requests broadcast channel info on a particular channel, 
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_QUERY_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // the indice of the channel we're looking at, 
+        uint16_t ch = item->data[ptr + 4];
+        // if the ch exists, 
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS && broadcastChannels[ch] != nullptr){
+          payload[wptr ++] = 1;
+          // now... these are route objects, but we only use the path part... 
+          // but we'll re-use route-object serialization schemes from EP_ROUTE_QUERY_REQ 
+          ts_writeUint16(broadcastChannels[ch]->ttl, payload, &wptr);
+          ts_writeUint16(broadcastChannels[ch]->segSize, payload, &wptr);
+          // path copy 
+          memcpy(&(payload[wptr]), broadcastChannels[ch]->path, broadcastChannels[ch]->pathLen);
+          wptr += broadcastChannels[ch]->pathLen;
+        } else {
+          payload[wptr ++] = 0;
+        }
+        // write reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_SET_REQ:
+      // mvc requests to set a broadcast channel route 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // ch to write into...
+        uint8_t ch = item->data[ptr + 4];
+        // reply-write-pointer 
+        uint16_t wptr = 0;
+        // prep a response, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_SET_RES;
+        payload[wptr ++] = id;
+        if(ch >= VBUS_MAX_BROADCAST_CHANNELS){
+          // won't go 
+          OSAP::error("attempt to write to oob broadcast channel");
+          payload[wptr ++] = 0;
+        } else {
+          // should go 
+          payload[wptr ++] = 1;          
+          if(broadcastChannels[ch] != nullptr) OSAP::debug("overwriting previous broadcast ch at " + String(ch));
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          setBroadcastChannel(ch, new Route(path, pathLen, ttl, segSize));
+        }
+        // in any case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_RM_REQ:
+      // mvc requests to rm a broadcast channel, 
+      // todo / cleanliness: might be salient to 'write 0' to delete (?) who knows 
+      {
+        // id & indice to rm 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t ch = item->data[ptr + 4];
+        uint16_t wptr = 0;
+        // prep res 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_RM_RES;
+        payload[wptr ++] = id;
+        // can we rm ?
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS){
+          if(broadcastChannels[ch] != nullptr) {
+            delete broadcastChannels[ch];
+            broadcastChannels[ch] = nullptr;
+            payload[wptr ++] = 1;
+          } else {
+            // didn't exist, so, a bad delete: 
+            payload[wptr ++] = 0;
+          }
+        } else {
+          // bad req, should throw errors... 
+          payload[wptr ++] = 0;
+        }
+        // can send now, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    default:
+      OSAP::error("vbus rx msg w/ unrecognized vbus key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/core/vertex.h b/system/firmware/lpf-axl-stepper/src/osape/core/vertex.h
new file mode 100644
index 0000000000000000000000000000000000000000..842d5733f64fa6661165c84e2193b2c0604892d1
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/core/vertex.h
@@ -0,0 +1,131 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "routes.h"
+#include "stack.h"
+// vertex config is build dependent, define in <folder-containing-osape>/osapConfig.h 
+#include "./osap_config.h" 
+
+// we have the vertex type, 
+// since it contains ptrs to others of its type, we fwd declare the type...
+class Vertex;
+// ... 
+typedef struct stackItem stackItem;
+typedef struct VPort VPort;
+typedef struct VBus VBus;
+
+// default vt fns 
+void vtLoopDefault(Vertex* vt);
+void vtOnOriginStackClearDefault(Vertex* vt, uint8_t slot);
+void vtOnDestinationStackClearDefault(Vertex* vt, uint8_t slot);
+
+// addressable node in the graph ! 
+class Vertex {
+  public:
+    // just temporary stashes, used all over the place to prep messages... 
+    static uint8_t payload[VT_SLOTSIZE];
+    static uint8_t datagram[VT_SLOTSIZE];
+    // -------------------------------- FN PTRS 
+    // these are *genuine function ptrs* not member functions, my dudes 
+    void (*loop_cb)(Vertex* vt) = nullptr;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    void (*onDestinationStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    // -------------------------------- Methods
+    virtual void loop(void);
+    virtual void destHandler(stackItem* item, uint16_t ptr);
+    void pingRequestHandler(stackItem* item, uint16_t ptr);
+    void scopeRequestHandler(stackItem* item, uint16_t ptr);
+    virtual void onOriginStackClear(uint8_t slot);
+    virtual void onDestinationStackClear(uint8_t slot);
+    // -------------------------------- DATA
+    // a type, a position, a name 
+    uint8_t type = VT_TYPE_CODE;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we were last scoped (need for graph traversals, final implementation tbd)
+    uint32_t scopeTimeTag = 0;
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    Vertex* parent = nullptr;
+    Vertex* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // sometimes a vertex is a vport, sometimes it is a vbus, 
+    VPort* vport;
+    VBus* vbus;
+    // -------------------------------- CONSTRUCTORS 
+    Vertex( 
+      Vertex* _parent, 
+      String _name, 
+      void (*_loop)(Vertex* vt),
+      void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+      void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+    );
+    Vertex(Vertex* _parent, String _name) : Vertex(_parent, _name, nullptr, nullptr, nullptr){};
+    Vertex(String _name) : Vertex(nullptr, _name, nullptr, nullptr, nullptr){};
+};
+
+// ---------------------------------------------- VPort 
+
+class VPort : public Vertex {
+  public:
+    // -------------------------------- OK these bbs are methods, 
+    virtual void send(uint8_t* data, uint16_t len) = 0;
+    virtual boolean cts(void) = 0;
+    virtual boolean isOpen(void) = 0;
+    // base constructor, 
+    VPort(Vertex* _parent, String _name);
+};
+
+// ---------------------------------------------- VBus 
+
+class VBus : public Vertex{
+  public:
+    // -------------------------------- Methods: these are purely virtual... 
+    virtual void send(uint8_t* data, uint16_t len, uint8_t rxAddr) = 0;
+    virtual void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) = 0;
+    // clear to send, clear to broadcast, 
+    virtual boolean cts(uint8_t rxAddr) = 0;
+    virtual boolean ctb(uint8_t broadcastChannel) = 0;
+    // link state per rx-addr,
+    virtual boolean isOpen(uint8_t rxAddr) = 0;
+    // handle things aimed at us, for mvc etc 
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // busses can read-in to broadcasts,
+    void injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel);
+    // we have also... broadcast channels... these are little route stubs & channel pairs, which we just straight up index, 
+    Route* broadcastChannels[VBUS_MAX_BROADCAST_CHANNELS];
+    // have to update those... 
+    void setBroadcastChannel(uint8_t channel, Route* route);
+    // has an rx addr, 
+    uint16_t ownRxAddr = 0;
+    // has a width-of-addr-space, 
+    uint16_t addrSpaceSize = 0;
+    // base constructor, children inherit... 
+    VBus(Vertex* _parent, String _name);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.cpp b/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81cc05bb3b38d85273a838a4b05df31bff2783a9
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.h b/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.h
new file mode 100644
index 0000000000000000000000000000000000000000..b47070ca26d021f113da680a6835df65712d4007
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.cpp b/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d9fe310be794e69ef9040e2ee33a26bcf986f9
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.cpp
@@ -0,0 +1,351 @@
+/*
+osape/vertices/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "../core/osap.h"
+#include "../core/packets.h"
+
+// -------------------------------------------------------- Constructors 
+
+// route constructor 
+EndpointRoute::EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+  if(_mode != EP_ROUTEMODE_ACKED && _mode != EP_ROUTEMODE_ACKLESS){
+    _mode = EP_ROUTEMODE_ACKLESS;
+  }
+  route = _route;
+  ackMode = _mode;
+  timeoutLength = _timeoutLength;
+}
+
+EndpointRoute::~EndpointRoute(void){
+  delete route;
+}
+
+// base constructor, 
+Endpoint::Endpoint(
+  Vertex* _parent, String _name, 
+  EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+  boolean (*_beforeQuery)(void)
+) : Vertex(_parent, "ep_" + _name) {
+  // type, 
+	type = VT_TYPE_ENDPOINT;
+  // set callbacks,
+  if(_onData) onData_cb = _onData;
+  if(_beforeQuery) beforeQuery_cb = _beforeQuery;
+}
+
+// -------------------------------------------------------- Dummies / Defaults 
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+// -------------------------------------------------------- Endpoint Route / Write API 
+
+void Endpoint::write(uint8_t* _data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(data, _data, len);
+  dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state == EP_TX_AWAITING_ACK){
+      routes[r]->state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      routes[r]->state = EP_TX_FRESH;
+    }
+  }
+}
+
+// add a route to an endpoint, returns indice where it's dropped, 
+uint8_t Endpoint::addRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+	// guard against more-than-allowed routes 
+	if(numRoutes >= ENDPOINT_MAX_ROUTES) {
+    OSAP::error("route add is oob", MEDIUM); 
+    return 0;
+	}
+  // build, stash, increment 
+  uint8_t indice = numRoutes;
+  routes[numRoutes ++] = new EndpointRoute(_route, _mode, _timeoutLength);
+  return indice; 
+}
+
+boolean Endpoint::clearToWrite(void){
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+// -------------------------------------------------------- Loop 
+
+void Endpoint::loop(void){
+  // ok we are doing a time-based dispatch... 
+  unsigned long now = millis();
+  EndpointRoute* routeTxList[ENDPOINT_MAX_ROUTES];
+  uint8_t numTxRoutes = 0;
+  // stack fresh routes, and also transition timeouts / etc, 
+  // we make & sort this list, but set it up round-robin, since many 
+  // cases will see the same TTL & same write-to time, meaning routes that 
+  // happen to be in low indices would chance on "higher priority" 
+  uint8_t r = lastRouteServiced;
+  for(uint8_t i = 0; i < numRoutes; i ++){
+    r ++; if(r >= numRoutes) r = 0;
+    switch(routes[r]->state){
+      case EP_TX_FRESH:
+        routeTxList[numTxRoutes ++] = routes[r];
+        break;
+      case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_FRESH;
+        }
+      default:
+        // noop for IDLE / otherwise...
+        break;
+    }
+  }
+  // now, would do a sort... they're all fresh at the same time, so lowest TTL would win,
+  // this one we would want to be stable, meaning original order is preserved in 
+  // otherwise identical cases, since we round-robin fairness as well as TTL / TTD  
+  #warning no sort algo yet, 
+  // serve 'em... these are all EP_TX_FRESH state, 
+  for(r = 0; r < numTxRoutes; r ++){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // make sure we'll have enough space...
+      if(dataLen + routeTxList[r]->route->pathLen + 3 >= VT_SLOTSIZE){
+        OSAP::error("attempting to write oversized datagram at " + name, MEDIUM);
+        routeTxList[r]->state = EP_TX_IDLE;
+        continue;
+      }
+      // write dest key, mode key, & id if acked, 
+      uint16_t wptr = 0;
+      payload[wptr ++] = PK_DEST;
+      if(routeTxList[r]->ackMode == EP_ROUTEMODE_ACKLESS){
+        payload[wptr ++] = EP_SS_ACKLESS;
+      } else {
+        payload[wptr ++] = EP_SS_ACKED;
+        payload[wptr ++] = nextAckID;
+        routeTxList[r]->ackId = nextAckID;
+        nextAckID ++;
+      } 
+      // write data into the payload, 
+      memcpy(&(payload[wptr]), data, dataLen);
+      wptr += dataLen;
+      // write the packet, 
+      uint16_t len = writeDatagram(datagram, VT_SLOTSIZE, routeTxList[r]->route, payload, wptr);
+      // tx time is now, and state is awaiting ack, 
+      routeTxList[r]->lastTxTime = now;
+      routeTxList[r]->state = EP_TX_AWAITING_ACK;
+      lastRouteServiced = r;
+      // ingest it...
+      stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len);
+    } else {
+      // stack has no more empty slots, bail from the loop, 
+      break;
+    }
+  } // end fresh-tx-awaiting state checks, 
+}
+
+// -------------------------------------------------------- Destination Handler  
+
+void Endpoint::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == EP_KEY, ptr + 3 = ID (if ack req.) 
+  switch(item->data[ptr + 2]){
+    case EP_SS_ACKLESS:
+      { // singlesegment transmit-to-us, w/o ack, 
+        uint8_t* rxData = &(item->data[ptr + 3]); uint16_t rxLen = item->len - (ptr + 4);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+        switch(resp){
+          case EP_ONDATA_WAIT:    // in a wait case, we no-op / escape, it comes back around 
+            item->arrivalTime = millis();
+            break;
+          case EP_ONDATA_ACCEPT:  // here we copy it in, but carry on to the reject term to delete og gram
+            memcpy(data, rxData, rxLen);
+            dataLen = rxLen;
+          case EP_ONDATA_REJECT:  // here we simply reject it, 
+            stackClearSlot(item);
+            break;
+        } // end resp-handler, 
+      }
+      break;
+    case EP_SS_ACKED:
+      { // singlesegment transmit-to-us, w/ ack, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t* rxData = &(item->data[ptr + 4]); uint16_t rxLen = item->len - (ptr + 5);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+          switch(resp){
+            case EP_ONDATA_WAIT: // this is a little danger-danger, 
+              item->arrivalTime = millis();
+              break;
+            case EP_ONDATA_ACCEPT:
+              memcpy(data, rxData, rxLen);
+              dataLen = rxLen;
+            case EP_ONDATA_REJECT:
+              // write the ack, ship it, 
+              payload[0] = PK_DEST;
+              payload[1] = EP_SS_ACK;
+              payload[2] = id;
+              uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 3);
+              stackClearSlot(item);
+              stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+              break;
+          }
+      }
+      break;
+    case EP_QUERY:
+      {
+        // beforeQuery, 
+        beforeQuery_cb();
+        // request for our data, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_QUERY_RESP;
+        payload[2] = item->data[ptr + 3];
+        memcpy(&(payload[3]), data, dataLen);
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, dataLen + 3);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_SS_ACK:
+      // acks to us, 
+      for(uint8_t r = 0; r < numRoutes; r ++){
+        if(item->data[ptr + 3] == routes[r]->ackId){
+          switch(routes[r]->state){
+            case EP_TX_AWAITING_ACK:
+              routes[r]->state = EP_TX_IDLE;
+              goto ackEnd;
+            case EP_TX_AWAITING_AND_FRESH:
+              routes[r]->state = EP_TX_FRESH;
+              goto ackEnd;
+            case EP_TX_FRESH:
+            case EP_TX_IDLE:
+            default:
+              // these are nonsense states, likely double-transmits, likely safely ignored,
+              goto ackEnd;
+          } // end switch 
+        }
+      } // end for-each route, if we've reached this point, still dump it;
+      ackEnd:
+      stackClearSlot(item);
+      break;
+    case EP_ROUTE_QUERY_REQ:
+      // MVC request for a route of ours, 
+      {
+        uint8_t id = item->data[ptr + 3];
+        uint16_t r = ts_readUint16(item->data, ptr + 4);
+        uint16_t wptr = 0;
+        // dest, key, id... mode, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_QUERY_RES;
+        payload[wptr ++] = id;
+        if(r < numRoutes){
+          payload[wptr ++] = routes[r]->ackMode;
+          // ttl, segsize, 
+          ts_writeUint16(routes[r]->route->ttl, payload, &wptr);
+          ts_writeUint16(routes[r]->route->segSize, payload, &wptr);
+          // path ! 
+          memcpy(&(payload[wptr]), routes[r]->route->path, routes[r]->route->pathLen);
+          wptr += routes[r]->route->pathLen;
+        } else {
+          payload[wptr ++] = 0; // no-route-here, 
+        }
+        // clear request, write reply in place, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_SET_REQ:
+      // MVC request to set a new route, 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_SET_RES;
+        payload[2] = id;
+        if(numRoutes + 1 <= ENDPOINT_MAX_ROUTES){
+          // tell call-er it should work, 
+          payload[3] = 1;
+          // gather & set route, 
+          uint8_t mode = item->data[ptr + 4];
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          OSAP::debug("adding path... w/ ttl " + String(ttl) + " ss " + String(segSize) + " pathLen " + String(pathLen));
+          uint8_t routeIndice = addRoute(new Route(path, pathLen, ttl, segSize), mode);
+          payload[4] = routeIndice;
+        } else {
+          // nope, 
+          payload[3] = 0;
+          payload[4] = 0;
+        }
+        // either case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 5);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_RM_REQ:
+      // MVC request to rm a route... 
+      {
+        // msg id, & indice to remove, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t r = item->data[ptr + 4];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_RM_RES;
+        payload[2] = id;
+        if(r < numRoutes){
+          // RM ok, 
+          payload[3] = 1;
+          // delete / run destructor 
+          delete routes[r];
+          // shift...
+          for(uint8_t i = r; i < numRoutes - 1; i ++){
+            routes[i] = routes[i + 1];
+          }
+          // last is null, 
+          routes[numRoutes] = nullptr;
+          numRoutes --;
+        } else {
+          // rm not-ok
+          payload[3] = 0;
+        }
+        // either case, write reply 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 4);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    default:
+      OSAP::error("endpoint rx msg w/ unrecognized endpoint key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } // end switch... 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.h b/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.h
new file mode 100644
index 0000000000000000000000000000000000000000..b14e45a64f1346b4e034d853343a336fd75c59aa
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape/vertices/endpoint.h
@@ -0,0 +1,98 @@
+/*
+osap/vertices/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "../core/vertex.h"
+#include "../core/packets.h"
+
+// ---------------------------------------------- Endpoint Routes, extends OSAP Core Routes 
+
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+class EndpointRoute {
+  public: 
+    Route* route;
+    uint8_t ackId = 0;
+    uint8_t ackMode = EP_ROUTEMODE_ACKLESS;
+    EP_ROUTE_STATES state = EP_TX_IDLE;
+    uint32_t lastTxTime = 0;
+    uint32_t timeoutLength;
+    // constructor, 
+    EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength = 1000);
+    // destructor...
+    ~EndpointRoute(void);
+};
+
+// ---------------------------------------------- Endpoints 
+
+// endpoint handler responses must be one of these enum - 
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+// default handlers, 
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len);
+boolean beforeQueryDefault(void);
+
+class Endpoint : public Vertex {
+  public:
+    // local data store & length, 
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t dataLen = 0; 
+    // callbacks: on new data & before a query is written out 
+    EP_ONDATA_RESPONSES (*onData_cb)(uint8_t* data, uint16_t len) = onDataDefault;
+    boolean (*beforeQuery_cb)(void) = beforeQueryDefault;
+    // we override vertex loop, 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // methods,
+    void write(uint8_t* _data, uint16_t len);
+    boolean clearToWrite(void);
+    uint8_t addRoute(Route* _route, uint8_t _mode = EP_ROUTEMODE_ACKLESS, uint32_t _timeoutLength = 1000);
+    // routes, for tx-ing to:
+    EndpointRoute* routes[ENDPOINT_MAX_ROUTES];
+    uint16_t numRoutes = 0;
+    uint16_t lastRouteServiced = 0;
+    uint8_t nextAckID = 77;
+    // base constructor, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+      boolean (*_beforeQuery)(void)
+    );
+    // these are called "delegating constructors" ... best reference is 
+    // here: https://en.cppreference.com/w/cpp/language/constructor 
+    // onData only, 
+    Endpoint(   
+      Vertex* _parent, String _name,
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len)
+    ) : Endpoint ( 
+      _parent, _name, _onData, nullptr
+    ){};
+    // beforeQuery only, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      boolean (*_beforeQuery)(void)
+    ) : Endpoint (
+      _parent, _name, nullptr, _beforeQuery
+    ){};
+    // name only, 
+    Endpoint(   
+      Vertex* _parent, String _name
+    ) : Endpoint (
+      _parent, _name, nullptr, nullptr
+    ){};
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/LICENSE.md b/system/firmware/lpf-axl-stepper/src/osape_arduino/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/README.md b/system/firmware/lpf-axl-stepper/src/osape_arduino/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da4c90cb6b618b1b8206b0ddf40a240acbaa4ca7
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/README.md
@@ -0,0 +1,7 @@
+## OSAP Arduino
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+It does not do anything on its own; this one builds helper classes to turn Arduino `Serial` and `Wire` objects into *virtual ports* and *virtual busses* respectively. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.cpp b/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8634694ff54bf2f45fdc704f3fd961168f4620bb
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.cpp
@@ -0,0 +1,77 @@
+/*
+arduino-ports/vp_arduinoWire.cpp
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#include "vb_arduinoWire.h"
+
+// static stash: same per instance, 
+uint8_t stash[32];
+uint8_t stashLen = 0;
+
+VBus_ArduinoWire::VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr
+) : VBus ( _parent, _name ) {
+  wire = _wire;
+  ownRxAddr = _ownRxAddr;
+}
+
+void VBus_ArduinoWire::begin(void){
+  wire->begin(ownRxAddr);
+  wire->onReceive(this->onRecieve);
+}
+
+void VBus_ArduinoWire::onRecieve(int count){
+  Wire.readBytes(stash, count);
+  stashLen = count;
+}
+
+void VBus_ArduinoWire::loop(void){
+  // check incoming, 
+  if(stashLen > 0){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      stackLoadSlot(this, VT_STACK_ORIGIN, stash, stashLen);
+    }
+    stashLen = 0;
+  }
+}
+
+void VBus_ArduinoWire::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  digitalWrite(A1, HIGH);
+  // this'll be the big hangup, 
+  if(len > 32) return;
+  // this might guard, if we are already rx'ing... 
+  if(wire->available()) return;
+  // become host, 
+  wire->end();
+  wire->begin();
+  // transmit, 
+  wire->beginTransmission(rxAddr);
+  wire->write(data, len);
+  uint8_t res = wire->endTransmission();
+  // become guest again, 
+  wire->end();
+  wire->begin(ownRxAddr);
+  // check, 
+  //if(res != 0) 
+  // DEBUG("res " + String(res) + " txd " + String(len));
+  digitalWrite(A1, LOW);
+}
+
+boolean VBus_ArduinoWire::cts(uint8_t rxAddr){
+  return true;
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.h b/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.h
new file mode 100644
index 0000000000000000000000000000000000000000..b098634545544070b65a46e1106719567f2fbe5b
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/vb_arduinoWire.h
@@ -0,0 +1,43 @@
+/*
+arduino-ports/vp_arduinoWire.h
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#ifndef ARDU_WIRELINK_H_
+#define ARDU_WIRELINK_H_
+
+#include <Arduino.h>
+#include <Wire.h>
+#include "../osape/core/vertex.h"
+
+#define WIRELINK_BUFSIZE 255 
+
+class VBus_ArduinoWire : public VBus {
+  public:
+    void begin(void);
+    // -------------------------------- our own loop, cts, and send... 
+    void loop(void) override; 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override; 
+    boolean cts(uint8_t rxAddr) override; 
+    // -------------------------------- data 
+    TwoWire* wire;
+    static void onRecieve(int count);
+    // -------------------------------- constructors
+    VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr);
+};
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.cpp b/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f71fe57592eccba322baf9108b38d068a2aed544
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.cpp
@@ -0,0 +1,174 @@
+/*
+arduino-ports/ardu-vport.h
+
+turns serial objects into competent link layers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "vp_arduinoSerial.h"
+#include "./osape/utils/cobs.h"
+#include "../osape/core/osap.h"
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Uart* _uart
+) : VPort ( _parent, _name ){
+  stream = _uart; // should convert Uart* to Stream*, as Uart inherits stream 
+  uart = _uart; 
+}
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Serial_* _usbcdc
+) : VPort ( _parent, _name ){
+  stream = _usbcdc;
+  usbcdc = _usbcdc;
+}
+
+void VPort_ArduinoSerial::begin(uint32_t baudRate){
+  if(uart != nullptr){
+    uart->begin(baudRate);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(baudRate); 
+  }
+}
+
+void VPort_ArduinoSerial::begin(void){
+  if(uart != nullptr){
+    uart->begin(1000000);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(9600);  // baud ignored on cdc begin  
+  }
+}
+
+// link packets are max 256 bytes in length, including the 0 delimiter 
+// structured like:
+// checksum | pck/ack key | pck id | cobs encoded data | 0 
+
+void VPort_ArduinoSerial::loop(void){
+  // byte injestion: think of this like the rx interrupt stage, 
+  while(stream->available()){
+    // read byte into the current stub, 
+    rxBuffer[rxBufferWp ++] = stream->read();
+    if(rxBuffer[rxBufferWp - 1] == 0){
+      // always reset keepalive last-rx time, 
+      lastRxTime = millis();
+      // 1st, we checksum:
+      if(rxBuffer[0] != rxBufferWp){ 
+        OSAP::error("serLink bad checksum, cs: " + String(rxBuffer[0]) + " wp: " + String(rxBufferWp), MINOR);
+      } else {
+        // acks, packs, or broken things 
+        switch(rxBuffer[1]){
+          case SERLINK_KEY_PCK:
+            // dirty guard for retransmitted packets, 
+            if(rxBuffer[2] != lastIdRxd){
+              inAwaitingId = rxBuffer[2]; // stash ID 
+              inAwaitingLen = cobsDecode(&(rxBuffer[3]), rxBufferWp - 2, inAwaiting); // fill inAwaiting 
+            } else {
+              OSAP::error("serLink double rx", MINOR);
+            }
+            break;
+          case SERLINK_KEY_ACK:
+            if(rxBuffer[2] == outAwaitingId){
+              outAwaitingLen = 0;
+            }
+            break;
+          case SERLINK_KEY_KEEPALIVE:
+            // noop, 
+            break;
+          default:
+            // makes no sense, 
+            break;
+        }
+      }
+      // always reset on delimiter, 
+      rxBufferWp = 0;
+    }
+  } // end while-receive 
+
+  // check insertion & genny the ack if we can 
+  if(inAwaitingLen && stackEmptySlot(this, VT_STACK_ORIGIN) && !ackIsAwaiting){
+    stackLoadSlot(this, VT_STACK_ORIGIN, inAwaiting, inAwaitingLen);
+    ackIsAwaiting = true;
+    ackAwaiting[0] = 4;                 // checksum still, innit 
+    ackAwaiting[1] = SERLINK_KEY_ACK;   // it's an ack bruv 
+    ackAwaiting[2] = inAwaitingId;      // which pck r we akkin m8 
+    ackAwaiting[3] = 0;                 // delimiter 
+    inAwaitingLen = 0;
+  }
+
+  // check & execute actual tx 
+  checkOutputStates();
+}
+
+void VPort_ArduinoSerial::send(uint8_t* data, uint16_t len){
+  //digitalWrite(A4, !digitalRead(A4));
+  // double guard?
+  if(!cts()) return;
+  // setup, 
+  outAwaiting[0] = len + 5;               // pck[0] is checksum = len + checksum + cobs start + cobs delimit + ack/pack + id 
+  outAwaiting[1] = SERLINK_KEY_PCK;       // this ones a packet m8 
+  outAwaitingId ++; if(outAwaitingId == 0) outAwaitingId = 1;
+  outAwaiting[2] = outAwaitingId;         // an id     
+  cobsEncode(data, len, &(outAwaiting[3]));  // encode 
+  outAwaiting[len + 4] = 0;               // stuff delimiter, 
+  outAwaitingLen = outAwaiting[0];        // track... 
+  // transmit attempts etc 
+  outAwaitingNTA = 0;
+  outAwaitingLTAT = 0;
+  // try it 
+  checkOutputStates();                    // try / start write 
+}
+
+// we are CTS if outPck is not occupied, 
+boolean VPort_ArduinoSerial::cts(void){
+  return (outAwaitingLen == 0);
+}
+
+// we are open if we've heard back lately, 
+boolean VPort_ArduinoSerial::isOpen(void){
+  return (millis() - lastRxTime < SERLINK_KEEPALIVE_RX_TIME && lastRxTime != 0);
+}
+
+void VPort_ArduinoSerial::checkOutputStates(void){
+  if(ackIsAwaiting && txBufferLen == 0){   // can we ack? 
+    memcpy(txBuffer, ackAwaiting, 4);
+    txBufferLen = 4;
+    lastTxTime = millis();
+    txBufferRp = 0;
+    ackIsAwaiting = false;
+  } else if(outAwaitingLen > 0 && txBufferLen == 0){   // would we be clear to tx ? 
+    // check retransmit cases, 
+    if(outAwaitingLTAT == 0 || outAwaitingLTAT + SERLINK_RETRY_TIME < micros()){
+      memcpy(txBuffer, outAwaiting, outAwaitingLen);
+      outAwaitingLTAT = micros();
+      txBufferLen = outAwaitingLen;
+      lastTxTime = millis();
+      txBufferRp = 0;
+      outAwaitingNTA ++;
+    } 
+    // check if last attempt, 
+    if(outAwaitingNTA >= SERLINK_RETRY_MACOUNT){
+      outAwaitingLen = 0;
+    }
+  } else if (millis() - lastTxTime > SERLINK_KEEPALIVE_TX_TIME && txBufferLen == 0){
+    //OSAP::debug("keepalive-ing " + name + " " + String(isOpen()));
+    memcpy(txBuffer, keepAlivePacket, 3);
+    txBufferLen = 3;
+    lastTxTime = millis();
+  }
+  // finally, we write out so long as we can: 
+  // we aren't guaranteed to get whole pckts out in each fn call 
+  while(stream->availableForWrite() && txBufferLen != 0){
+    // output next byte, 
+    stream->write(txBuffer[txBufferRp ++]);
+    // check for end of buffer; reset transmit states if so 
+    if(txBufferRp >= txBufferLen) {
+      txBufferLen = 0; 
+      txBufferRp = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.h b/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa518aabc7e8905a85abf8ec07d4a2138b2f10f2
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_arduino/vp_arduinoSerial.h
@@ -0,0 +1,88 @@
+/*
+arduino-ports/vp_arduinoSerial.h
+
+turns arduino serial objects into competent link layers, for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ARDU_SERLINK_H_
+#define ARDU_SERLINK_H_
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+#define SERLINK_BUFSIZE 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+#define SERLINK_SEGSIZE SERLINK_BUFSIZE - 5
+// packet keys; 
+#define SERLINK_KEY_PCK 170  // 0b10101010
+#define SERLINK_KEY_ACK 171  // 0b10101011
+#define SERLINK_KEY_KEEPALIVE 173 
+// retry settings 
+#define SERLINK_RETRY_MACOUNT 2
+#define SERLINK_RETRY_TIME 100000  // microseconds 
+#define SERLINK_KEEPALIVE_TX_TIME 800 // milliseconds 
+#define SERLINK_KEEPALIVE_RX_TIME 1200 // ms 
+
+#define SERLINK_LIGHT_ON_TIME 100 // in ms 
+
+// note that we use uint8_t write ptrs / etc: and a size of 255, 
+// so we are never dealing w/ wraps etc, god bless 
+
+class VPort_ArduinoSerial : public VPort {
+  public:
+    // arduino std begin 
+    void begin(uint32_t baud);
+    void begin(void);
+    // -------------------------------- our own gd send & cts & loop fns, 
+    void loop(void) override;
+    void checkOutputStates(void);
+    void send(uint8_t* data, uint16_t len) override;
+    boolean cts(void) override;
+    boolean isOpen(void) override;
+    // -------------------------------- Data 
+    // Uart & USB are both Stream classes, 
+    Stream* stream;
+    // we have an overloaded constructor w/ uart or Serial_, the usb class 
+    Uart* uart = nullptr;
+    Serial_* usbcdc = nullptr; 
+    // incoming, always kept clear to receive: 
+    uint8_t rxBuffer[SERLINK_BUFSIZE];
+    uint8_t rxBufferWp = 0;
+    // keepalive state, 
+    uint32_t lastRxTime = 0;
+    uint32_t lastTxTime = 0;
+    uint8_t keepAlivePacket[3] = {3, SERLINK_KEY_KEEPALIVE, 0};
+    // guard on double transmits 
+    uint8_t lastIdRxd = 0;
+    // incoming stash
+    uint8_t inAwaiting[SERLINK_BUFSIZE];
+    uint8_t inAwaitingId = 0;
+    uint8_t inAwaitingLen = 0;
+    // outgoing ack, 
+    uint8_t ackAwaiting[4];
+    boolean ackIsAwaiting = false;
+    // outgoing await,
+    uint8_t outAwaiting[SERLINK_BUFSIZE];
+    uint8_t outAwaitingId = 1;
+    uint8_t outAwaitingLen = 0;
+    uint8_t outAwaitingNTA = 0;
+    unsigned long outAwaitingLTAT = 0;
+    // outgoing buffer,
+    uint8_t txBuffer[SERLINK_BUFSIZE];
+    uint8_t txBufferLen = 0;
+    uint8_t txBufferRp = 0;
+    // -------------------------------- Constructors 
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Uart* _uart);
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Serial_* _usbcdc);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/README.md b/system/firmware/lpf-axl-stepper/src/osape_ucbus/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e5a9fae5795a46730372cd9533efa958bc12c2e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/README.md
@@ -0,0 +1,6 @@
+## UART-Clocked Bus Submodule 
+
+https://gitlab.cba.mit.edu/jakeread/ucbus 
+https://github.com/jakeread/ucbus 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.cpp b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2bd443fa0154b4e62e4392611b7afabb257fb
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.cpp
@@ -0,0 +1,510 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "ucBusDipConfig.h"
+#include "../indicators.h"
+#include "../osape/core/osap.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+// last-time-rx'd 
+volatile uint32_t lastRxTime = 0;
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts: not the highest priority (0), just under. 
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  NVIC_SetPriority(SERCOM1_IRQn, 1);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // track keepalive 
+    lastRxTime = millis();
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { 
+    OSAP::error("ucbus-drop outsize numToken rx", MINOR); 
+    return; 
+  }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    OSAP::error("ucbus-drop rx overfull buffer", MINOR);
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || rxCh == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        OSAP::error("ucbus-drop rx FC fails on ch " + String(rxCh), MINOR);
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        // ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        // inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+boolean ucBusDrop_ctrA(void){
+  // likewise
+  return (inBufferLen[0] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+size_t ucBusDrop_readA(uint8_t* dest){
+  if(!ucBusDrop_ctrA()) return 0;
+  // we read out the whole gd thing,
+  size_t len = inBufferLen[0];
+  memcpy(dest, &(inBuffer[0]), len);
+  inBufferLen[0] = 0; // now empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusDrop_isPresent(uint8_t drop){
+  // can't tx anywhere other than to head, 
+  if(drop > 0) return false;
+  return (millis() - lastRxTime < UB_KEEPALIVE_TIME);
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..281f430bd6ced5264fd9607ce8f51a1a9c31cbab
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusDrop.h
@@ -0,0 +1,51 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t* dest);
+boolean ucBusDrop_ctrA(void);
+size_t ucBusDrop_readA(uint8_t* dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+boolean ucBusDrop_isPresent(uint8_t rxAddr);
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.cpp b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..854e488395920dd19b812643282ddbf9c7f3ae25
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.cpp
@@ -0,0 +1,386 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../osape/core/osap.h"
+#include "./utils_samd51/peripheral_nums.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// last-rx'd-time, per drop presence-detect, 
+volatile uint32_t lastRxTime[UB_MAX_DROPS];
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin, also set lastRxTime to zero for each, 
+  for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+    lastRxTime[d] = 0;
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      outBufferLen[ch] = 0;
+      outBufferRp[ch] = 0;
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (channel '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] || ch == 0)){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update keepalive: last we heard from this drop:
+    lastRxTime[rxDrop] = millis();
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { 
+      OSAP::error("ucbus-head outsize numToken rx", MEDIUM); 
+      return; 
+    }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ 
+      OSAP::error("ucbus-head rx FC broken", MEDIUM); 
+      return; 
+    }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      OSAP::error("ucbus-head rx packet too-long", MEDIUM);
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero / are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusHead_isPresent(uint8_t drop){
+  if(drop > UCBUS_MAX_DROPS) return false;
+  return (millis() - lastRxTime[drop] < UB_KEEPALIVE_TIME);
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: channel ID
+  outBuffer[0][0] = channel;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..65f43edcfc482f9656fe30d0bf7f7ea0f9c1eb67
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+boolean ucBusHead_isPresent(uint8_t drop); // have we heard from this drop recently ? 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusMacros.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusMacros.h
new file mode 100644
index 0000000000000000000000000000000000000000..72f3f0c0b60e6c7efac390db2e6db4be7e9b133a
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucBusMacros.h
@@ -0,0 +1,127 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "./ucbus_config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// time-until-considered-dead, in ms  
+#define UB_KEEPALIVE_TIME 200 
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL 2 // A: 0, B: 1, C: 2
+#define UB_RXPERIPHERAL 2
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.cpp b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08742fdc5cda9435f8ad54b76a4f85c81c433b1e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..97ec2b5750e86bbd6d98acd3ef42c02b489240f4
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.cpp b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a49901b40751839e972e4f5d778179834dba1868
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.cpp
@@ -0,0 +1,95 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusDrop.h"
+#include "../osape/core/osap.h"
+
+// badness, direct write in future 
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusDrop::VBus_UCBusDrop(Vertex* _parent, String _name
+): VBus(_parent, _name){
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusDrop::begin(void){
+  ucBusDrop_setup(true, 0);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::begin(uint8_t _ownRxAddr){
+  ucBusDrop_setup(false, _ownRxAddr);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::loop(void){
+  // can we shift-in from channel a / broadcast messages ?
+  // also... stack 'em from the broadcast channel first, typically higher priority 
+  if(ucBusDrop_ctrA()){
+    // and if we have an empty space... 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+    // get len & strip out the broadcastChannel, which was stuffed at [0]
+    uint16_t len = ucBusDrop_readA(_tempBuffer);
+    injestBroadcastPacket(&(_tempBuffer[1]), len - 1, _tempBuffer[0]);
+    }
+  }
+  // can we shift-in from channel b / directed messages ? 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+void VBus_UCBusDrop::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    OSAP::error("ubd tx while not clear", MEDIUM);
+  }
+}
+
+boolean VBus_UCBusDrop::cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  return (rxAddr == 0 && ucBusDrop_ctsB());
+}
+
+void VBus_UCBusDrop::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  OSAP::debug("Broadcast is unwritten");
+}
+
+boolean VBus_UCBusDrop::ctb(uint8_t broadcastChannel){
+  OSAP::debug("Bus Drop CTB is unwritten");
+  return false;
+}
+
+boolean VBus_UCBusDrop::isOpen(uint8_t rxAddr){
+  return ucBusDrop_isPresent(rxAddr);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4333e6491b0439d01ae4bc480bce37af864f5
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusDrop.h
@@ -0,0 +1,41 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VBUS_UCBUS_HEAD_H_
+#define VBUS_UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusDrop : public VBus {
+  public:
+    void begin(void);
+    void begin(uint8_t _ownRxAddr);
+    void loop(void) override;
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr);
+    VBus_UCBusDrop(Vertex* _parent, String _name);
+};
+
+#endif 
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.cpp b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0e5cd5676e138fa0c17215c075181594ff48ac
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.cpp
@@ -0,0 +1,93 @@
+/*
+osap/vb_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusHead.h"
+#include "../osape/core/osap.h"
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusHead::VBus_UCBusHead(Vertex* _parent, String _name
+): VBus (_parent, _name) {
+  // report our address size,
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusHead::begin(void){
+  // start ucbus
+  ucBusHead_setup(); 
+}
+
+void VBus_UCBusHead::loop(void){
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      if (stackEmptySlot(this, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+void VBus_UCBusHead::timerISR(void){
+  ucBusHead_timerISR();
+}
+
+void VBus_UCBusHead::send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    OSAP::error("attempt to busf from head to self", MEDIUM);
+  } else {  
+    ucBusHead_transmitB(data, len, rxAddr);
+  }
+}
+
+boolean VBus_UCBusHead::cts(uint8_t rxAddr){
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void VBus_UCBusHead::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  ucBusHead_transmitA(data, len, broadcastChannel);
+}
+
+boolean VBus_UCBusHead::ctb(uint8_t broadcastChannel){
+  return ucBusHead_ctsA();
+}
+
+boolean VBus_UCBusHead::isOpen(uint8_t rxAddr){
+  return ucBusHead_isPresent(rxAddr);
+}
+
+#endif 
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.h b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfb7829f135f8ea04f193d3657f38cb15ea63cfa
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/osape_ucbus/vb_ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/vb_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusHead : public VBus {
+  public:
+    void begin(void);
+    // loop to ferry data, 
+    void loop(void) override;
+    // fast loop, needs to be called in ~ 10kHz ISR 
+    void timerISR(void);
+    // ... bus : osap API 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr) override;
+    // -------------------------------- Constructors 
+    VBus_UCBusHead(Vertex* _parent, String _name);
+};
+
+#endif
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/ucbus_config.h b/system/firmware/lpf-axl-stepper/src/ucbus_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..836480877313326c2aee681285e47a91be175db7
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/ucbus_config.h
@@ -0,0 +1,29 @@
+/*
+ucbus_config.h
+
+config options for an ucbus instance 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_CONFIG_H_
+#define UCBUS_CONFIG_H_
+
+#define UCBUS_MAX_DROPS 32 
+#define UCBUS_IS_DROP 
+//#define UCBUS_IS_HEAD 
+
+#define UCBUS_BAUD 2 
+
+#define UCBUS_IS_D51
+// #define UCBUS_IS_D21
+
+#define UCBUS_ON_OSAP 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/utils_samd51/README.md b/system/firmware/lpf-axl-stepper/src/utils_samd51/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a5e4922e5be8001ad756c57dc6cd5c934ca1572e
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/utils_samd51/README.md
@@ -0,0 +1,3 @@
+## ATSAMD51 Utes
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.cpp b/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387cbaad46e7f17a5f553c446a47558abbc20bc7
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.cpp
@@ -0,0 +1,129 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "clock_utils.h"
+#include "../indicators.h"
+
+/*
+// I used to have this singleton stuff here, but I think
+// since I am using the extern... I have no need for it, 
+D51ClockUtils* D51ClockUtils::instance = 0;
+
+D51ClockUtils* D51ClockUtils::getInstance(void){
+    if(instance == 0){
+        instance = new D51ClockUtils();
+    }
+    return instance;
+}
+
+D51ClockUtils* D51ClockUtils = D51ClockUtils::getInstance();
+*/
+
+D51ClockUtils* d51ClockUtils;
+
+D51ClockUtils::D51ClockUtils(){}
+
+void D51ClockUtils::setup_16mhz_xtal(void){
+    if(mhz_xtal_is_setup) return; // already done, 
+    // let's make a clock w/ that xtal:
+    OSCCTRL->XOSCCTRL[0].bit.RUNSTDBY = 0;
+    OSCCTRL->XOSCCTRL[0].bit.XTALEN = 1;
+    // set oscillator current..
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_IMULT(4) | OSCCTRL_XOSCCTRL_IPTAT(3);
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_STARTUP(5);
+    OSCCTRL->XOSCCTRL[0].bit.ENALC = 1;
+    OSCCTRL->XOSCCTRL[0].bit.ENABLE = 1;
+    // make the peripheral clock available on this ch 
+    GCLK->GENCTRL[MHZ_XTAL_GCLK_NUM].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_XOSC0) | GCLK_GENCTRL_GENEN;  // GCLK_GENCTRL_SRC_DFLL
+    while (GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(MHZ_XTAL_GCLK_NUM)){
+        //DEBUG2PIN_TOGGLE;
+    };
+    mhz_xtal_is_setup = true;
+}
+
+void D51ClockUtils::start_ticker_a(uint32_t us){
+    //now using 120mHz main clock (gen(0)) instead of xtal, 
+    //setup_16mhz_xtal();
+    // ok
+    TC0->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC1->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0 | MCLK_APBAMASK_TC1;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC0->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC1->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC0->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC1->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC0->COUNT32.INTENSET.bit.MC0 = 1;
+    TC0->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC0->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 100kHz, ok? 
+    if(us < 10) us = 10;
+    // 120 / 2 -> 60 ticks per us, 
+    TC0->COUNT32.CC[0].reg = 60 * us;
+    // enable, sync for enable write
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC0->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC1->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC0_IRQn);
+    NVIC_SetPriority(TC0_IRQn, 2);
+}
+
+void D51ClockUtils::set_ticker_a_priority(uint32_t prio){
+    if(prio > 3) prio = 3;
+    NVIC_SetPriority(TC0_IRQn, prio);
+}
+
+void D51ClockUtils::start_ticker_b(uint32_t us){
+    setup_16mhz_xtal();
+    // ok
+    TC2->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC3->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBBMASK.reg |= MCLK_APBBMASK_TC2 | MCLK_APBBMASK_TC3;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC2_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC3_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC2->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC3->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC2->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC3->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC2->COUNT32.INTENSET.bit.MC0 = 1;
+    TC2->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC2->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 1MHz, ok? 
+    if(us < 8) us = 8;
+    TC2->COUNT32.CC[0].reg = 8 * us;
+    // enable, sync for enable write
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC2->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC3->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC2_IRQn);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.h b/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1f9e7472c034dc3a1aee6fc995eb65043d660
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/utils_samd51/clock_utils.h
@@ -0,0 +1,45 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef CLOCKS_D51_H_
+#define CLOCKS_D51_H_
+
+#include <Arduino.h>
+
+#define MHZ_XTAL_GCLK_NUM 9
+
+class D51ClockUtils {
+    private:
+        static D51ClockUtils* instance;
+    public:
+        D51ClockUtils();
+        static D51ClockUtils* getInstance(void);
+        // xtal
+        volatile boolean mhz_xtal_is_setup = false;
+        uint32_t mhz_xtal_gclk_num = 9;
+        void setup_16mhz_xtal(void);
+        // uses TC0 and TC1 as 32 bit TC
+        // pickup TC0_Handler(void){}
+        // do in handler: 
+        // TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+        // TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+        // us: requested timer period 
+        void start_ticker_a(uint32_t us);
+        void set_ticker_a_priority(uint32_t prio);
+        // uses TC2 and TC3 as 32 bit TC 
+        // pickup on TC2_Handler(void){}
+        void start_ticker_b(uint32_t us);
+};
+
+extern D51ClockUtils* d51ClockUtils;
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/utils_samd51/peripheral_nums.h b/system/firmware/lpf-axl-stepper/src/utils_samd51/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/utils_samd51/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/src/utils_samd51/pin_macros.h b/system/firmware/lpf-axl-stepper/src/utils_samd51/pin_macros.h
new file mode 100644
index 0000000000000000000000000000000000000000..89418657d8a481cb20ec3532cbd3ef0488dda521
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/src/utils_samd51/pin_macros.h
@@ -0,0 +1,13 @@
+#ifndef PIN_MACROS_D51_H_
+#define PIN_MACROS_D51_H_
+
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+#define PIN_SETUP_INPUT(port, pin) PORT->Group[port].DIRCLR.reg = PIN_BM(pin); PORT->Group[port].PINCFG[pin].reg = PORT_PINCFG_INEN
+#define PIN_SETUP_PULLEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PULLEN = 1
+#define PIN_SETUP_PMUXEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PMUXEN = 1
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-axl-stepper/test/README b/system/firmware/lpf-axl-stepper/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..9b1e87bc67c90e7f09a92a3e855444b085c655a6
--- /dev/null
+++ b/system/firmware/lpf-axl-stepper/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Test Runner and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
diff --git a/system/firmware/lpf-filament-sensor/.gitignore b/system/firmware/lpf-filament-sensor/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/system/firmware/lpf-filament-sensor/.vscode/extensions.json b/system/firmware/lpf-filament-sensor/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/system/firmware/lpf-filament-sensor/include/README b/system/firmware/lpf-filament-sensor/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/system/firmware/lpf-filament-sensor/lib/README b/system/firmware/lpf-filament-sensor/lib/README
new file mode 100644
index 0000000000000000000000000000000000000000..6debab1e8b4c3faa0d06f4ff44bce343ce2cdcbf
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/system/firmware/lpf-filament-sensor/platformio.ini b/system/firmware/lpf-filament-sensor/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..6e9090faa74070843cd794299e632a6a4b99d123
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/platformio.ini
@@ -0,0 +1,14 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:adafruit_gemma_m0]
+platform = atmelsam
+board = adafruit_gemma_m0
+framework = arduino
diff --git a/system/firmware/lpf-filament-sensor/src/drivers/as5047.cpp b/system/firmware/lpf-filament-sensor/src/drivers/as5047.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..743408d82b34bfbf9b9b7a847d5c001ea963d00c
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/drivers/as5047.cpp
@@ -0,0 +1,92 @@
+/*
+drivers/as5047.h
+
+reads an as5047x on SER0 on SAMD21
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "as5047.h"
+#include "../indicators.h"
+
+/* 
+we have the AS5047 on
+PA08: 0-0 MOSI
+PA09: 0-1 CLK
+PA10: 0-2 CS
+PA11: 0-3 MISO 
+*/
+
+#define SER0SPI SERCOM0->SPI
+
+void as5047_setup(void){
+  // pin configs, 
+  PIN_SETUP_OUTPUT(0, 8);
+  PIN_SETUP_OUTPUT(0, 9);
+  PIN_SETUP_OUTPUT(0, 10);
+  PIN_SETUP_INPUT(0, 11);
+  // mux pins, 
+  PORT_WRCONFIG_Type wrconfig;
+  wrconfig.bit.WRPMUX = 1;          // writes to pmux, 
+  wrconfig.bit.WRPINCFG = 1;        // writes to pinconfig 
+  wrconfig.bit.PMUX = MUX_PA08C_SERCOM0_PAD0;
+  wrconfig.bit.PMUXEN = 1;          // writing to pmux... 
+  wrconfig.bit.HWSEL = 0;           // writing to lower half
+  wrconfig.bit.PINMASK = PIN_BM(8); // mask at pin 8, 
+  PORT->Group[0].WRCONFIG.reg = wrconfig.reg; 
+  wrconfig.bit.PMUX = MUX_PA09C_SERCOM0_PAD1;
+  wrconfig.bit.PINMASK = PIN_BM(9); // mask at pin 9, 
+  // PIN10 is CS, we'll operate it manually... 
+  // PORT->Group[0].WRCONFIG.reg = wrconfig.reg; 
+  // wrconfig.bit.PMUX = MUX_PA10C_SERCOM0_PAD2;
+  // wrconfig.bit.PINMASK = PIN_BM(10); // mask at pin 10, 
+  PORT->Group[0].WRCONFIG.reg = wrconfig.reg; 
+  wrconfig.bit.PMUX = MUX_PA11C_SERCOM0_PAD3;
+  wrconfig.bit.PINMASK = PIN_BM(11); // mask at pin 11, 
+  PORT->Group[0].WRCONFIG.reg = wrconfig.reg; 
+  // ok... now setup the sercom, right? 
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM0;
+  // hook that up to main CPU clock, 
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM0_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // do the actual setup... 
+  SER0SPI.CTRLA.bit.SWRST = 1;
+  while(SER0SPI.SYNCBUSY.bit.SWRST);
+  // and setup... 
+  SER0SPI.CTRLA.reg = //SERCOM_SPI_CTRLA_CPOL |
+                      SERCOM_SPI_CTRLA_CPHA |
+                      SERCOM_SPI_CTRLA_DIPO(3) |
+                      SERCOM_SPI_CTRLA_DOPO(0) |
+                      SERCOM_SPI_CTRLA_MODE(3);
+  SER0SPI.CTRLB.reg = SERCOM_SPI_CTRLB_RXEN;
+  SER0SPI.BAUD.reg = SERCOM_SPI_BAUD_BAUD(12);
+  // enable ! 
+  SER0SPI.CTRLA.bit.ENABLE = 1;
+  while(SER0SPI.SYNCBUSY.bit.ENABLE);
+}
+
+// 00 : ? 
+// 01 : ? 
+// 11 : ?
+// 10 : ?
+
+uint16_t as5047_transact(uint16_t outWord){
+  // probbo *not* going to just blind toggle, now that we're hooked up to peripheral 
+  uint16_t inWord = 0;
+  PIN_LO(0, 10);
+  SER0SPI.DATA.reg = (outWord >> 8);
+  while(!SER0SPI.INTFLAG.bit.TXC);
+  inWord |=  SER0SPI.DATA.reg << 8;
+  SER0SPI.DATA.reg = (outWord & 255);
+  while(!SER0SPI.INTFLAG.bit.TXC);
+  inWord |= SER0SPI.DATA.reg & 255;
+  PIN_HI(0, 10);
+  return inWord;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/drivers/as5047.h b/system/firmware/lpf-filament-sensor/src/drivers/as5047.h
new file mode 100644
index 0000000000000000000000000000000000000000..11011d61828791bf9d8d5bd9f3d7a31cbee83deb
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/drivers/as5047.h
@@ -0,0 +1,38 @@
+/*
+drivers/as5047.h
+
+reads an as5047x on SER0 on SAMD21
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENC_AS5047_H_
+#define ENC_AS5047_H_
+
+#include <Arduino.h>
+
+#define AS5047_SPI_READ_POS (0b1100000000000000 | 0x3FFF)
+#define AS5047_SPI_READ_POS_UNC (0b1100000000000000 | 0x3FFE)
+#define AS5047_SPI_NO_OP (0b1000000000000000)
+
+#define AS5047_SPI_READ_MAG (0b1100000000000000 | 0x3FFD)
+#define AS5047_SPI_READ_DIAG (0b1100000000000000 | 0x3FFC)
+
+typedef struct {
+    boolean magHi;
+    boolean magLo;
+    boolean cordicOverflow;
+    boolean compensationComplete;
+    uint8_t angularGainCorrection;
+} as5047_diag_t;
+
+void as5047_setup(void);
+uint16_t as5047_transact(uint16_t outWord);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/indicators.h b/system/firmware/lpf-filament-sensor/src/indicators.h
new file mode 100644
index 0000000000000000000000000000000000000000..a17994f7a030e18a101e243522cae94d6679b5ba
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/indicators.h
@@ -0,0 +1,24 @@
+// circuit specific indicators: D21 filament sensor, w/ ucbus interface 
+
+// pin helper macros 
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+#define PIN_SETUP_INPUT(port, pin) PORT->Group[port].DIRCLR.reg = PIN_BM(pin)
+
+// PA23: clock 
+#define CLKLIGHT_ON PIN_HI(0, 23)
+#define CLKLIGHT_OFF PIN_LO(0, 23)
+#define CLKLIGHT_TOGGLE PIN_TGL(0, 23)
+#define CLKLIGHT_SETUP PIN_SETUP_OUTPUT(0, 23); CLKLIGHT_OFF
+
+// PA22: bus light 
+#define BUSLIGHT_ON PIN_HI(0, 22)
+#define BUSLIGHT_OFF PIN_LO(0, 22)
+#define BUSLIGHT_TOGGLE PIN_TGL(0, 22)
+#define BUSLIGHT_SETUP PIN_SETUP_OUTPUT(0, 22); BUSLIGHT_OFF
+
+#define ERRLIGHT_ON 
+#define ERRLIGHT_OFF 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/main.cpp b/system/firmware/lpf-filament-sensor/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a3a38e15ec4af2d65d37a71e4ace74aa75c65675
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/main.cpp
@@ -0,0 +1,163 @@
+#include <Arduino.h>
+#include "indicators.h"
+#include "drivers/as5047.h"
+
+#include "osape/core/osap.h"
+#include "osape/vertices/endpoint.h"
+#include "osape_arduino/vp_arduinoSerial.h"
+#include "osape_ucbus/vb_ucBusDrop.h"
+
+// -------------------------------------------------------- HALL SENSOR CODE 
+
+// we'll run the LERP calibration & filtering here, since we are going to be delivering 
+// this data to some other embedded element that does the actual control... 
+
+float hallEstimate = 0.0f;   // current estimate 
+float hallAlpha = 0.1f;         // trust in new data 
+
+void updateHallEstimate(void){
+  float newObservation = analogRead(A0);
+  hallEstimate = newObservation * hallAlpha + hallEstimate * (1 - hallAlpha);
+}
+
+// lerp data,  
+#define LERP_DATA_POINTS 4 
+float lerp_diameters[LERP_DATA_POINTS] = { 1.0f, 1.5f, 1.8f, 2.4f };
+float lerp_readings[LERP_DATA_POINTS] = { 2500.0f, 2013.0f , 1851.0f , 1547.0f };
+
+float lerp(float reading){
+  // find bounds:
+  boolean found = false; 
+  uint8_t bound = 0;
+  for(uint8_t i = 0; i < LERP_DATA_POINTS - 1; i ++){
+    if(reading < lerp_readings[i] && reading > lerp_readings[i + 1]){
+      found = true;
+      bound = i;
+      break;
+    }
+  }
+  if(found){
+    // get posn-in-readings-span, should be 0-1... 
+    float inSpan = (lerp_readings[bound] - reading) / (lerp_readings[bound] - lerp_readings[bound + 1]);
+    // calculate span-plus 
+    float val = lerp_diameters[bound] + inSpan * (lerp_diameters[bound + 1] - lerp_diameters[bound]);
+    return val;
+  } else {
+    return 0.0f;
+  }
+}
+
+// -------------------------------------------------------- 10ms (100hz) loop... 
+
+// tick in MS
+#define UPDATE_TICK 10
+// wrapping aids, 
+uint16_t lastReading = 0;
+uint16_t encoderRange = 16383;
+int32_t encoderWraps = 0;
+// ute, 
+float floatReading = 0.0F;
+// estimates 
+float posReading = 0.0F;
+float posEstimate = 0.0F;
+float lastPosEstimate = 0.0F;
+float posAlpha = 0.2F; // as trust in new measurements 
+float rateEstimate = 0.0F;
+float rateAlpha = 0.05F;
+float delT = 0.001F * (float)UPDATE_TICK;
+
+void sensorLoop(void){
+  // update & filter this, 
+  updateHallEstimate();
+  // get a new reading (transacting twice for freshy data) 
+  // 1st bit is parity IIRC, so... 
+  uint16_t reading = as5047_transact(AS5047_SPI_READ_POS);
+  reading = as5047_transact(AS5047_SPI_READ_POS) & 0b0011111111111111;
+  // calculate for wraps, 
+  if(reading < 1000 && lastReading > encoderRange - 1000){
+    encoderWraps ++;
+  } else if (reading > encoderRange - 1000 && lastReading < 1000){
+    encoderWraps --;
+  }
+  lastReading = reading; 
+  // let's go from 0-16383 to 0-2PI, floating points, for these maths:
+  // i.e. store it all as radians, 
+  floatReading = ((float)(reading) * TWO_PI) / 16383.0F;
+  // the complete position is the wrap-count... plus the current reading, 
+  posReading = ((float)encoderWraps * TWO_PI) + floatReading;
+  // filter that... 
+  posEstimate = (float)posReading * posAlpha + posEstimate * (1 - posAlpha);
+  // rates... 
+  rateEstimate = ((posEstimate - lastPosEstimate) / delT) * rateAlpha + rateEstimate * (1 - rateAlpha);
+  lastPosEstimate = posEstimate;
+}
+
+// -------------------------------------------------------- OSAP 
+
+OSAP osap("filament-sensor");
+
+// -------------------------------------------------------- VPORTS 
+
+VPort_ArduinoSerial vpUSBSerial(&osap, "arduinoUSBSerial", &Serial);
+
+VBus_UCBusDrop vbUCBusDrop(&osap, "ucBusDrop");
+
+// ---------------------------------------------- HALL ENDPOINT 
+
+Endpoint bundleEP(&osap, "bundle");
+
+uint8_t dataBytes[16];       // temp write buffer 
+
+void updateEndpoint(void){
+  // LERP the current observation 
+  float lerpd = lerp(hallEstimate);
+  // write -> bytes, 
+  uint16_t wptr = 0;
+  ts_writeFloat32(lerpd, dataBytes, &wptr);
+  ts_writeFloat32(posEstimate, dataBytes, &wptr);
+  ts_writeFloat32(rateEstimate, dataBytes, &wptr);
+  // now write that to the endpoint... 
+  bundleEP.write(dataBytes, 16);
+}
+
+// -------------------------------------------------------- SYSTEM SETUP 
+
+void setup() {
+  //while(!Serial);
+  // setup indicators 
+  CLKLIGHT_SETUP;
+  BUSLIGHT_SETUP;
+  // hall effect doesn't need setup... arduino does that ?
+  analogReference(AR_INTERNAL2V0);
+  analogReadResolution(12);
+  // setup comms 
+  vpUSBSerial.begin();
+  vbUCBusDrop.begin(12);
+  // setup AS5047 werk
+  as5047_setup();
+}
+
+// in ms, how often to update hall 
+#define PUBLISH_DIV 5
+unsigned long last_tick = 0;
+uint8_t divs = 0;
+
+void loop() {
+  // sys 
+  osap.loop();
+  // blink 
+  if(millis() > last_tick + UPDATE_TICK){
+    sensorLoop();
+    last_tick = millis();
+    divs ++; 
+    if(divs >= PUBLISH_DIV - 1){
+      divs = 0;
+      BUSLIGHT_TOGGLE;
+      updateEndpoint();
+    }
+  }
+}
+
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len){} 
+
+void ucBusDrop_onRxISR(void){} 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osap_config.h b/system/firmware/lpf-filament-sensor/src/osap_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..7c85ffd6a155b91cfff856af9e901039223219e0
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osap_config.h
@@ -0,0 +1,36 @@
+/*
+osap_config.h
+
+config options for an osap-embedded build 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_CONFIG_H_
+#define OSAP_CONFIG_H_
+
+// size of vertex stacks, lenght, then count,
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 3  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+#define VT_MAXITEMSPERTURN 8
+
+// max # of endpoints that could be spawned here,
+#define MAX_CONTEXT_ENDPOINTS 64
+
+// count of routes each endpoint can have, 
+#define ENDPOINT_MAX_ROUTES 4
+#define ENDPOINT_ROUTE_MAX_LEN 64 
+
+#define VBUS_MAX_BROADCAST_CHANNELS 32
+
+// if this is defined, please also provide an osap_debug.h 
+#define OSAP_DEBUG 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/LICENSE.md b/system/firmware/lpf-filament-sensor/src/osape/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/README.md b/system/firmware/lpf-filament-sensor/src/osape/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c94ebaff92a9980dbc93aa25047846ee4aa64e0
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/README.md
@@ -0,0 +1,5 @@
+## OSAP Embedded 
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/loop.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/loop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c050974467d2fc95677d72f2e2da3b6608a0f588
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/loop.cpp
@@ -0,0 +1,255 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "loop.h"
+#include "packets.h"
+#include "osap.h"
+
+#define MAX_ITEMS_PER_LOOP 32
+//#define LOOP_DEBUG
+
+// we'll stack up to 64 messages to handle per loop, 
+// more items would cause issues: will throw errors and design circular looping at that point 
+stackItem* itemList[MAX_ITEMS_PER_LOOP];
+uint16_t itemListLen = 0;
+
+void listSetupRecursor(Vertex* vt){
+  // run the vertex' loop... but not if it's the root, yar 
+  if(vt->type != VT_TYPE_ROOT) vt->loop();
+  // for each input / output stack, try to collect all items... 
+  // alright I'm doing this collect... but want a kind of pickup-where-you-left-off thing, 
+  // so that we can have a fixed-length loop, i.e. 64 items per, but still do fairness... 
+  // otherwise our itemList has to be large enough to carry potentially every single item ? 
+  for(uint8_t od = 0; od < 2; od ++){
+    uint8_t count = stackGetItems(vt, od, &(itemList[itemListLen]), MAX_ITEMS_PER_LOOP - itemListLen);
+    itemListLen += count;
+  }
+  // recurse children...
+  for(uint8_t c = 0; c < vt->numChildren; c ++){
+    listSetupRecursor(vt->children[c]);
+  }
+}
+
+// sort-in-place based on time-to-death, 
+void listSort(stackItem** list, uint16_t listLen){
+  // write each item's time-to-death, 
+  uint32_t now = millis();
+  for(uint16_t i = 0; i < listLen; i ++){
+    list[i]->timeToDeath = ts_readUint16(list[i]->data, 0) - (now - list[i]->arrivalTime);
+  }
+  // also... vertex arrivalTime should be uint32_t milliseconds of arrival... 
+  #warning not-yet sorted... 
+}
+
+// this handles internal transport... checking for errors along paths, and running flowcontrol 
+// returns true to wipe current item, false to leave-in-wait, 
+boolean internalTransport(stackItem* item, uint16_t ptr){
+  // we walk thru our little internal tree here, 
+  Vertex* vt = item->vt;
+  // ptr for the walk, use item->data[ptr] == PK_INSTRUCTION, not PK_PTR, 
+  uint16_t fwdPtr = ptr + 1;
+  // count # of ops, 
+  uint8_t opCount = 0;
+  // for a max. of 16 fwd steps, 
+  for(uint8_t s = 0; s < 16; s ++){
+    uint16_t arg = readArg(item->data, fwdPtr);
+    switch(PK_READKEY(item->data[fwdPtr])){
+      // ---------------------------------------- Internal Dir Cases 
+      case PK_SIB:
+        // check validity of route & shift our reference vt,
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during sib transport"); return true;
+        } else if (arg >= vt->parent->numChildren){
+          OSAP::error("no sibling " + String(arg) + " at " + vt->name + " during sib transport"); return true;
+        } else {
+          // this is it: we go fwds to this vt & end-of-switch statements increment ptrs
+          vt = vt->parent->children[arg];
+        }
+        break;
+      case PK_PARENT:
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during parent transport"); return true;
+        } else {
+          // likewise... 
+          vt = vt->parent;
+        }
+        break;
+      case PK_CHILD:
+        if(arg >= vt->numChildren){
+          OSAP::error("no child " + String(arg) + " at " + vt->name + " during child transport"); return true;
+        } else {
+          // again, just walk fwds... 
+          vt = vt->children[arg];
+        }
+        break;
+      // ---------------------------------------- Terminal / Exit Cases 
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD: 
+      case PK_DEST:
+      case PK_PINGREQ:
+      case PK_SCOPEREQ:
+      case PK_LLESCAPE:
+        // check / transport...
+        if(stackEmptySlot(vt, VT_STACK_DESTINATION)){
+          // walk the ptr fwds, 
+          walkPtr(item->data, item->vt, opCount, ptr);
+          // ingest at the new place, 
+          stackLoadSlot(vt, VT_STACK_DESTINATION, item->data, item->len);
+          // return true to clear it out, 
+          return true;
+        } else {
+          return false; 
+        }
+      default:
+        OSAP::error("internal transport failure, ptr walk ends at unknown key");
+        return true;
+    } // end switch 
+    fwdPtr += 2;
+    opCount ++;
+  } // end max-16-steps, 
+  // if we're past all 16 and didn't hit a terminal, pckt is eggregiously long, rm it 
+  return true;
+}
+
+// -------------------------------------------------------- LOOP Begins Here 
+
+// ... would be breadth-first, ideally 
+void osapLoop(Vertex* root){
+  // we want to build a list of items, recursing through... 
+  itemListLen = 0;
+  listSetupRecursor(root);
+  // check now if items are nearly oversized...
+  // see notes in the log from 2022-06-22 if this error occurs, 
+  if(itemListLen >= MAX_ITEMS_PER_LOOP - 2){
+    OSAP::error("loop items exceeds " + String(MAX_ITEMS_PER_LOOP) + ", breaking per-loop transport properties... pls fix", HALTING);
+  }
+  // stash high-water mark,
+  if(itemListLen > OSAP::loopItemsHighWaterMark) OSAP::loopItemsHighWaterMark = itemListLen;
+  // log 'em 
+  // OSAP::debug("list has " + String(itemListLen) + " elements", LOOP);
+  // otherwise we can carry on... the item should be sorted, global vars, 
+  listSort(itemList, itemListLen);
+  // then we can handle 'em one by one 
+  for(uint16_t i = 0; i < itemListLen; i ++){
+    osapItemHandler(itemList[i]);
+  }
+}
+
+void osapItemHandler(stackItem* item){
+  // clear dead items, 
+  if(item->timeToDeath < 0){
+    OSAP::debug(  "item at " + item->vt->name + " times out w/ " + String(item->timeToDeath) + 
+                  " ms to live, of " + String(ts_readUint16(item->data, 0)) + " ttl", LOOP);
+    stackClearSlot(item);
+    return;
+  }
+  // get a ptr for the item, 
+  uint16_t ptr = 0;
+  if(!findPtr(item->data, &ptr)){    
+    OSAP::error("item at " + item->vt->name + " unable to find ptr, deleting...");
+    stackClearSlot(item);
+    return;
+  }
+  // now the handle-switch, item->data[ptr] = PK_PTR, we switch on instruction which is behind that, 
+  switch(PK_READKEY(item->data[ptr + 1])){
+    // ------------------------------------------ Terminal / Destination Switches 
+    case PK_DEST:
+      item->vt->destHandler(item, ptr);
+      break;
+    case PK_PINGREQ:
+      item->vt->pingRequestHandler(item, ptr);
+      break;
+    case PK_SCOPEREQ:
+      item->vt->scopeRequestHandler(item, ptr);
+      break;
+    case PK_PINGRES:
+    case PK_SCOPERES:
+      OSAP::error("ping or scope request issued to " + item->vt->name + " not handling those in embedded", MEDIUM);
+      stackClearSlot(item);
+      break;
+    // ------------------------------------------ Internal Transport 
+    case PK_SIB:
+    case PK_PARENT:
+    case PK_CHILD:  // transport handler returns true if msg should be wiped, false if it should be cycled
+      if(internalTransport(item, ptr)){
+        stackClearSlot(item);
+      }
+      break;
+    // ------------------------------------------ Network Transport 
+    case PK_PFWD:
+      // port forward...
+      if(item->vt->vport == nullptr){
+        OSAP::error("pfwd to non-vport " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        if(item->vt->vport->cts()){
+          // walk one step, but only if fn returns true (having success) 
+          if(walkPtr(item->data, item->vt, 1, ptr)) item->vt->vport->send(item->data, item->len);
+          stackClearSlot(item);
+        } else {
+          // failed to send this turn (flow controlled), will return here next round 
+        }
+      }
+      break;
+    case PK_BFWD:
+    case PK_BBRD:
+      // bus forward / bus broadcast: 
+      if(item->vt->vbus == nullptr){
+        OSAP::error("bfwd to non-vbus " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        // arg is rxAddr for bus-forwards, is broadcastChannel for bus-broadcast, 
+        uint16_t arg = readArg(item->data, ptr + 1);
+        if(item->data[ptr + 1] == PK_BFWD){
+          if(item->vt->vbus->cts(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              item->vt->vbus->send(item->data, item->len, arg);
+            } else {
+              OSAP::error("bfwd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bfwd (flow controlled), returning here next round... 
+          }
+        } else if (item->data[ptr + 1] == PK_BBRD){
+          if(item->vt->vbus->ctb(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              // OSAP::debug("broadcasting on ch " + String(arg));
+              item->vt->vbus->broadcast(item->data, item->len, arg);
+            } else {
+              OSAP::error("bbrd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bbrd, returning next... 
+          }
+        } else {
+          // doesn't make any sense, we switched in on these terms... 
+          OSAP::error("absolute nonsense", MEDIUM);
+          stackClearSlot(item);
+        }
+      }
+      break;
+    case PK_LLESCAPE:
+      OSAP::error("lldebug to embedded, dumping", MINOR);
+      stackClearSlot(item);
+      break;
+    default:
+      OSAP::error("unrecognized ptr to " + item->vt->name + " " + String(PK_READKEY(item->data[ptr + 1])), MINOR);
+      stackClearSlot(item);
+      // error, delete, 
+      break;
+  } // end swiiiitch 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/loop.h b/system/firmware/lpf-filament-sensor/src/osape/core/loop.h
new file mode 100644
index 0000000000000000000000000000000000000000..5022aa16c00da6b40864ca8f09432dab0744ad04
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/loop.h
@@ -0,0 +1,25 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef LOOP_H_
+#define LOOP_H_ 
+
+#include "vertex.h"
+
+// we loop, 
+void osapLoop(Vertex* root);
+// we handle, 
+void osapItemHandler(stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/osap.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/osap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acde43271ecb27ea482e1b1079b02d847a15fed9
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/osap.cpp
@@ -0,0 +1,111 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "loop.h"
+#include "packets.h"
+#include "../utils/cobs.h"
+
+// stash most recents, and counts, and high water mark, 
+uint32_t OSAP::loopItemsHighWaterMark = 0;
+uint32_t errorCount = 0;
+uint32_t debugCount = 0;
+// strings...
+unsigned char latestError[VT_SLOTSIZE];
+unsigned char latestDebug[VT_SLOTSIZE];
+uint16_t latestErrorLen = 0;
+uint16_t latestDebugLen = 0;
+
+OSAP::OSAP(String _name) : Vertex("rt_" + _name){};
+
+void OSAP::loop(void){
+  // this is the root, so we kick all of the internal net operation from here 
+  osapLoop(this);
+}
+
+void OSAP::destHandler(stackItem* item, uint16_t ptr){
+  // classic switch on 'em 
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == ROOT_KEY, ptr + 3 = ID (if ack req.) 
+  uint16_t wptr = 0;
+  uint16_t len = 0;
+  switch(item->data[ptr + 2]){
+    case RT_DBG_STAT:
+    case RT_DBG_ERRMSG:
+    case RT_DBG_DBGMSG:
+      // return w/ the res key & same issuing ID 
+      payload[wptr ++] = PK_DEST;
+      payload[wptr ++] = RT_DBG_RES;
+      payload[wptr ++] = item->data[ptr + 3];
+      // stash high water mark, errormsg count, debugmsgcount 
+      ts_writeUint32(OSAP::loopItemsHighWaterMark, payload, &wptr);
+      ts_writeUint32(errorCount, payload, &wptr);
+      ts_writeUint32(debugCount, payload, &wptr);
+      // optionally, a string... I know we switch() then if(), it's uggo, 
+      if(item->data[ptr + 2] == RT_DBG_ERRMSG){
+        ts_writeString(latestError, latestErrorLen, payload, &wptr, VT_SLOTSIZE / 2);
+      } else if (item->data[ptr + 2] == RT_DBG_DBGMSG){
+        ts_writeString(latestDebug, latestDebugLen, payload, &wptr, VT_SLOTSIZE / 2);
+      }
+      // that's the payload, I figure, 
+      len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+      stackClearSlot(item);
+      stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      break;
+    default:
+      OSAP::error("unrecognized key to root node " + String(item->data[ptr + 2]));
+      stackClearSlot(item);
+      break;
+  }
+}
+
+uint8_t errBuf[255];
+uint8_t errBufEncoded[255];
+
+void debugPrint(String msg){
+  // whatever you want,
+  uint32_t len = msg.length();
+  // max this long, per the serlink bounds 
+  if(len + 9 > 255) len = 255 - 9;
+  // header... 
+  errBuf[0] = len + 8;  // len, key, cobs start + end, strlen (4) 
+  errBuf[1] = 172;      // serialLink debug key 
+  errBuf[2] = len & 255;
+  errBuf[3] = (len >> 8) & 255;
+  errBuf[4] = (len >> 16) & 255;
+  errBuf[5] = (len >> 24) & 255;
+  msg.getBytes(&(errBuf[6]), len + 1);
+  // encode from 2, leaving the len, key header... 
+  size_t ecl = cobsEncode(&(errBuf[2]), len + 4, errBufEncoded);
+  // what in god blazes ? copy back from encoded -> previous... 
+  memcpy(&(errBuf[2]), errBufEncoded, ecl);
+  // set tail to zero, to delineate, 
+  errBuf[errBuf[0] - 1] = 0;
+  // direct escape 
+  Serial.write(errBuf, errBuf[0]);
+}
+
+void OSAP::error(String msg, OSAPErrorLevels lvl){
+  //const char* str = msg.c_str();
+  msg.getBytes(latestError, VT_SLOTSIZE);
+  latestErrorLen = msg.length();
+  errorCount ++;
+  debugPrint(msg);
+}
+
+void OSAP::debug(String msg, OSAPDebugStreams stream){
+  msg.getBytes(latestDebug, VT_SLOTSIZE);
+  latestDebugLen = msg.length();
+  debugCount ++;
+  debugPrint(msg);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/osap.h b/system/firmware/lpf-filament-sensor/src/osape/core/osap.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c2c9d789ebd23ba452c7259c3423088ff2b9f
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/osap.h
@@ -0,0 +1,38 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+
+// largely semantic class, OSAP represents the root vertex in whichever context 
+// and it's where run the main loop from, etc... 
+// here is where we coordinate context-level stuff: adding new instances, 
+// stashing error messages & counts, etc, 
+
+enum OSAPErrorLevels { HALTING, MEDIUM, MINOR };
+enum OSAPDebugStreams { DEFAULT, LOOP };
+
+class OSAP : public Vertex {
+  public: 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr);
+    OSAP(String _name);// : Vertex(_name);
+    static void error(String msg, OSAPErrorLevels lvl = MINOR );
+    static void debug(String msg, OSAPDebugStreams stream = DEFAULT );
+    static uint32_t loopItemsHighWaterMark;
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/packets.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/packets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf83928d99d3c173d0efdef40ab614dc2433b409
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/packets.cpp
@@ -0,0 +1,193 @@
+/*
+osap/packets.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "packets.h"
+#include "ts.h"
+#include "osap.h"
+
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg){
+  buf[ptr] = key | (0b00001111 & (arg >> 8));
+  buf[ptr + 1] = arg & 0b11111111;
+}
+// not sure how I want to do this yet... 
+uint16_t readArg(uint8_t* buf, uint16_t ptr){
+  return ((buf[ptr] & 0b00001111) << 8) | buf[ptr + 1];
+}
+
+boolean findPtr(uint8_t* pck, uint16_t* pt){
+  // 1st instruction is always at pck[4], pck[0][1] == ttl, pck[2][3] == segSize 
+  uint16_t ptr = 4;
+  // there's a potential speedup where we assume given *pt is already incremented somewhat, 
+  // maybe shaves some ns... but here we just look fresh every time, 
+  for(uint8_t i = 0; i < 16; i ++){
+    switch(PK_READKEY(pck[ptr])){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        ptr += 2;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+boolean walkPtr(uint8_t* pck, Vertex* source, uint8_t steps, uint16_t ptr){
+  // if the ptr we were handed isn't in the right spot, try to find it... 
+  if(pck[ptr] != PK_PTR){
+    // if that fails, bail... 
+    if(!findPtr(pck, &ptr)){
+      OSAP::error("before a ptr walk, ptr is out of place...");
+      return false;
+    }
+  }
+  // carry on w/ the walking algo, 
+  for(uint8_t s = 0; s < steps; s ++){
+    switch PK_READKEY(pck[ptr + 1]){
+      case PK_SIB:
+        {
+          // stash indice from-whence it came,
+          uint16_t txIndice = source->indice;
+          // for loop's next step, this is the source now, 
+          source = source->parent->children[readArg(pck, ptr + 1)];
+          // where ptr is currently, we stash new key/pair for a reversal, 
+          writeKeyArgPair(pck, ptr, PK_SIB, txIndice);
+          // increment packet's ptr, and our own... 
+          pck[ptr + 2] = PK_PTR; 
+          ptr += 2;
+        }
+        break;
+      case PK_PARENT:
+        // reversal for a 'parent' instruction is to bounce back down to the child, 
+        writeKeyArgPair(pck, ptr, PK_CHILD, source->indice);
+        // next source is now...
+        source = source->parent;
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      case PK_CHILD:
+        // next source is... 
+        source = source->children[readArg(pck, ptr + 1)];
+        // reversal for 'child' instruction is to go back up to parent, 
+        writeKeyArgPair(pck, ptr, PK_PARENT, 0);
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2; 
+        break;
+      case PK_PFWD:
+        // reversal for pfwd instruction is identical, 
+        writeKeyArgPair(pck, ptr, PK_PFWD, 0);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // though this should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have port fwd w/ more than one step");
+          return false;
+        }
+        break;
+      case PK_BFWD:
+        // reversal for bfwd instruction is to return *up*... 
+        writeKeyArgPair(pck, ptr, PK_BFWD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // this also should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have bus fwd w/ more than one step");
+          return false; 
+        }
+        break;
+      case PK_BBRD:
+        // broadcasts are a little strange, we also stuff the ownRxAddr in,
+        writeKeyArgPair(pck, ptr, PK_BBRD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      default:
+        OSAP::error("have out of place keys in the ptr walk...");
+        return false;
+    }
+  } // end steps, alleged success,  
+  return true; 
+}
+
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen){
+  uint16_t wptr = 0;
+  ts_writeUint16(route->ttl, gram, &wptr);
+  ts_writeUint16(route->segSize, gram, &wptr);
+  memcpy(&(gram[wptr]), route->path, route->pathLen);
+  wptr += route->pathLen;
+  if(wptr + payloadLen > route->segSize){
+    OSAP::error("writeDatagram asked to write packet that exceeds segSize, bailing", MEDIUM);
+    return 0;
+  }
+  memcpy(&(gram[wptr]), payload, payloadLen);
+  wptr += payloadLen;
+  return wptr;
+}
+
+// original gram, payload, len, 
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen){
+  // 1st up, we can straight copy the 1st 4 bytes, 
+  memcpy(gram, ogGram, 4);
+  // now find a ptr, 
+  uint16_t ptr = 0;
+  if(!findPtr(ogGram, &ptr)){
+    OSAP::error("writeReply can't find the pointer...", MEDIUM);
+    return 0;
+  }
+  // do we have enough space? it's the minimum of the allowed segsize & stated maxGramLength, 
+  maxGramLength = min(maxGramLength, ts_readUint16(ogGram, 2));
+  if(ptr + 1 + payloadLen > maxGramLength){
+    OSAP::error("writeReply asked to write packet that exceeds maxGramLength, bailing", MEDIUM);
+    return 0;
+  }
+  // write the payload in, apres-pointer, 
+  memcpy(&(gram[ptr + 1]), payload, payloadLen);
+  // now we can do a little reversing... 
+  uint16_t wptr = 4;
+  uint16_t end = ptr;
+  uint16_t rptr = ptr;
+  // 1st byte... the ptr, 
+  gram[wptr ++] = PK_PTR;
+  // now for a max 16 steps, 
+  for(uint8_t h = 0; h < 16; h ++){
+    if(wptr >= end) break;
+    rptr -= 2;
+    switch(PK_READKEY(ogGram[rptr])){
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        gram[wptr ++] = ogGram[rptr];
+        gram[wptr ++] = ogGram[rptr + 1];
+        break;
+      default:
+        OSAP::error("writeReply fails to reverse this packet, bailing", MEDIUM);
+        return 0;
+    }
+  } // end thru-loop, 
+  // it's written, return the len  // we had gram[ptr] = PK_PTR, so len was ptr + 1, then added payloadLen, 
+  return end + 1 + payloadLen;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/packets.h b/system/firmware/lpf-filament-sensor/src/osape/core/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..914656be1eb7656f481915438a10701edad23280
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/packets.h
@@ -0,0 +1,48 @@
+/*
+osap/packets.h
+
+reading / writing from osap packets / datagrams 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_PACKETS_H_
+#define OSAP_PACKETS_H_
+
+#include <Arduino.h>
+#include "vertex.h"
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 240
+#define PK_DEST 224
+#define PK_PINGREQ 192 
+#define PK_PINGRES 176 
+#define PK_SCOPEREQ 160 
+#define PK_SCOPERES 144 
+#define PK_SIB 16 
+#define PK_PARENT 32 
+#define PK_CHILD 48 
+#define PK_PFWD 64 
+#define PK_BFWD 80
+#define PK_BBRD 96 
+#define PK_LLESCAPE 112 
+
+// to read *just the key* from key, arg pair
+#define PK_READKEY(data) (data & 0b11110000)
+
+// packet utes, 
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg);
+uint16_t readArg(uint8_t* buf, uint16_t ptr);
+boolean findPtr(uint8_t* pck, uint16_t* ptr);
+boolean walkPtr(uint8_t* pck, Vertex* vt, uint8_t steps, uint16_t ptr = 4);
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen);
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/routes.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/routes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6caea0c0a00c56f339f2fdb7ec4b02278e1faf73
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/routes.cpp
@@ -0,0 +1,55 @@
+/*
+osap/routes.cpp
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "routes.h"
+#include "packets.h"
+
+Route::Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize){
+  ttl = _ttl;
+  segSize = _segSize;
+  // nope, 
+  if(_pathLen > 64){
+    _pathLen = 0;
+  }
+  memcpy(path, _path, _pathLen);
+  pathLen = _pathLen;
+}
+
+Route::Route(void){
+  path[pathLen ++] = PK_PTR;
+}
+
+Route* Route::sib(uint16_t indice){
+  writeKeyArgPair(path, pathLen, PK_SIB, indice);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::pfwd(void){
+  writeKeyArgPair(path, pathLen, PK_PFWD, 0);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bfwd(uint16_t rxAddr){
+  writeKeyArgPair(path, pathLen, PK_BFWD, rxAddr);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bbrd(uint16_t channel){
+  writeKeyArgPair(path, pathLen, PK_BBRD, channel);
+  pathLen += 2;
+  return this; 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/routes.h b/system/firmware/lpf-filament-sensor/src/osape/core/routes.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2bb3c97cffb7df24867de4efe7489b40daa4a0e
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/routes.h
@@ -0,0 +1,38 @@
+/*
+osap/routes.h
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_ROUTES_H_
+#define OSAP_ROUTES_H_
+
+#include <Arduino.h>
+
+// a route type... 
+class Route {
+  public:
+    uint8_t path[64];
+    uint16_t pathLen = 0;
+    uint16_t ttl = 1000;
+    uint16_t segSize = 128;
+    // write-direct constructor, 
+    Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize);
+    // write-along constructor, 
+    Route(void);
+    // pass-thru initialize constructors, 
+    Route* sib(uint16_t indice);
+    Route* pfwd(void);
+    Route* bfwd(uint16_t rxAddr);
+    Route* bbrd(uint16_t channel);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/stack.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/stack.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..401bd7103f872bd141172d945ff2b2a8cb93e36f
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/stack.cpp
@@ -0,0 +1,138 @@
+/*
+osap/stack.cpp
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "stack.h"
+#include "vertex.h"
+#include "osap.h"
+
+// ---------------------------------------------- Stack Tools 
+
+void stackReset(Vertex* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+      // and ptrs to self, 
+      vt->stack[od][s].vt = vt;
+      vt->stack[od][s].od = od;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(Vertex* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //DEBUG("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item){
+  // this would be deadly, so:
+  if(od > 1) {
+    OSAP::error("stackClearSlot, od > 1, badness", MEDIUM);
+    return;
+  }
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+  }
+  // now we callback to the vertex; these fns are often used to clear flowcontrol condns 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      vt->onDestinationStackClear(indice);
+      break;
+    default:  // guarded against this above... 
+      break;
+  }
+}
+
+void stackClearSlot(stackItem* item){
+  stackClearSlot(item->vt, item->od, item);
+}
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/stack.h b/system/firmware/lpf-filament-sensor/src/osape/core/stack.h
new file mode 100644
index 0000000000000000000000000000000000000000..79151239b987f025150dc6f1ac580cfc4e474887
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/stack.h
@@ -0,0 +1,54 @@
+/*
+osap/stack.h
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STACK_H_
+#define STACK_H_ 
+
+#include <Arduino.h>
+#include "./osap_config.h" 
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+class Vertex;
+
+// core routing layer chunk-of-stuff, 
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+typedef struct stackItem {
+  uint8_t data[VT_SLOTSIZE];          // data bytes
+  uint16_t len = 0;                   // data bytes count 
+  uint32_t arrivalTime = 0;           // ms-since-system-alive, time at last ingest
+  int32_t timeToDeath = 0;            // ms of time until pckt vanishes on this hop
+  Vertex* vt;                         // vertex to whomst we belong, 
+  uint8_t od;                         // origin / destination to which we belong, 
+  uint8_t indice;                     // actual physical position in the stack 
+  uint16_t ptr = 0;                   // current data[ptr] == 88 
+  stackItem* next = nullptr;          // linked ringbuffer next 
+  stackItem* previous = nullptr;      // linked ringbuffer previous 
+} stackItem;
+
+// stack setup / reset 
+void stackReset(Vertex* vt);
+
+// stack origin side 
+boolean stackEmptySlot(Vertex* vt, uint8_t od);
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// stack exit side 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item);
+void stackClearSlot(stackItem* item);
+
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/ts.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/ts.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cd3fdc9c1c249d25b22b75fa9fc69f311d04c19
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/ts.cpp
@@ -0,0 +1,183 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+// ---------------------------------------------- Reading 
+
+// boolean 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr){
+  if(buf[(*ptr) ++]){
+    *val = true;
+  } else {
+    *val = false;
+  }
+}
+
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr){
+  boolean val = buf[(*ptr)] ? true : false;
+  (*ptr) += 1;
+  return val;
+}
+
+// uint8 
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr){
+  uint8_t val = buf[(*ptr)];
+  (*ptr) += 1;
+  return val;
+}
+
+// uint16 
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+#warning some of these are pretty vague, i.e. this ingests a pointer *not as a pointer* (lol)
+// so it doesn't increment it, whereas the readUint8 above *does so* - ... ?? pick a style ? 
+uint16_t ts_readUint16(unsigned char* buf, uint16_t ptr){
+  return (buf[ptr + 1] << 8) | buf[ptr];
+}
+
+// uint32 
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr){
+  uint32_t val = (buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)]);
+  (*ptr) += 4;
+  return val;
+}
+
+// int32 
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.i;
+}
+
+// float32 
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.f;
+}
+
+// -------------------------------------------------------- Writing 
+
+// boolean
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+// unsigned 
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+// signed 
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int16 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+}
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+// floats 
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+// string, overloaded ?
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen){
+  if(strLen > maxLen) strLen = maxLen;
+  buf[(*ptr) ++] = strLen & 255;
+  buf[(*ptr) ++] = (strLen >> 8) & 255;
+  buf[(*ptr) ++] = (strLen >> 16) & 255;
+  buf[(*ptr) ++] = (strLen >> 24) & 255;
+  // write in one-by-one, surely there is a better way, 
+  for(uint16_t i = 0; i < strLen; i ++){
+    buf[(*ptr) ++] = str[i];
+  }
+  *ptr += strLen;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/ts.h b/system/firmware/lpf-filament-sensor/src/osape/core/ts.h
new file mode 100644
index 0000000000000000000000000000000000000000..63e77b2b02c0f7716bb1cba55cc9eef613d1207f
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/ts.h
@@ -0,0 +1,157 @@
+/*
+core/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22       // top level 
+#define VT_TYPE_MODULE 23     // collection of things (?) or something, idk yet 
+#define VT_TYPE_ENDPOINT 24   // software endpoint w/ read/write semantics 
+#define VT_TYPE_QUERY 25 
+#define VT_TYPE_ENDPOINT_MULTISEG 26 // likewise, but requring multisegment transmission 
+#define VT_TYPE_CODE 25       // autonomous graph dwellers 
+#define VT_TYPE_VPORT 44      // virtual ports 
+#define VT_TYPE_VBUS 45       // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+#define EP_ROUTE_QUERY_REQ 141 
+#define EP_ROUTE_QUERY_RES 142
+#define EP_ROUTE_SET_REQ 143
+#define EP_ROUTE_SET_RES 144 
+#define EP_ROUTE_RM_REQ 147
+#define EP_ROUTE_RM_RES 148 
+
+#define EP_ROUTEMODE_ACKED 167
+#define EP_ROUTEMODE_ACKLESS 168 
+
+// -------------------------------------------------------- Root Keys 
+
+#define RT_DBG_STAT 151
+#define RT_DBG_ERRMSG 152 
+#define RT_DBG_DBGMSG 153
+#define RT_DBG_RES 161
+
+// -------------------------------------------------------- VBus MVC Keys 
+
+#define VBUS_BROADCAST_MAP_REQ 145
+#define VBUS_BROADCAST_MAP_RES 146
+#define VBUS_BROADCAST_QUERY_REQ 141
+#define VBUS_BROADCAST_QUERY_RES 142
+#define VBUS_BROADCAST_SET_REQ 143
+#define VBUS_BROADCAST_SET_RES 144 
+#define VBUS_BROADCAST_RM_REQ 147 
+#define VBUS_BROADCAST_RM_RES 148 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int16 {
+  uint8_t bytes[2];
+  int16_t i;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr);
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr);
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+uint16_t ts_readUint16(uint8_t* buf, uint16_t ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr);
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+// -------------------------------------------------------- Writing 
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/vertex.cpp b/system/firmware/lpf-filament-sensor/src/osape/core/vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9ce012af681a42b059c6585888d1db806dd2ab51
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/vertex.cpp
@@ -0,0 +1,327 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "stack.h"
+#include "osap.h"
+#include "packets.h"
+
+// ---------------------------------------------- Temporary Stash 
+
+uint8_t Vertex::payload[VT_SLOTSIZE];
+uint8_t Vertex::datagram[VT_SLOTSIZE];
+
+// ---------------------------------------------- Vertex Constructor and Defaults 
+
+Vertex::Vertex( 
+  Vertex* _parent, String _name, 
+  void (*_loop)(Vertex* vt),
+  void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+  void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+){
+  // name self, reset stack... 
+  name = _name;
+  stackReset(this);
+  // callback assignments... 
+  loop_cb = _loop;
+  onOriginStackClear_cb = _onOriginStackClear;
+  onDestinationStackClear_cb = _onDestinationStackClear;
+  // insert self to osap net,
+  if(_parent == nullptr){
+    type = VT_TYPE_ROOT;
+    indice = 0;
+  } else {
+    if (_parent->numChildren >= VT_MAXCHILDREN) {
+      OSAP::error("trying to nest a vertex under " + _parent->name + " but we have reached VT_MAXCHILDREN limit", HALTING);
+    } else {
+      this->indice = _parent->numChildren;
+      this->parent = _parent;
+      _parent->children[_parent->numChildren ++] = this;
+    }
+  }
+}
+
+void Vertex::loop(void){
+  if(loop_cb != nullptr) return loop_cb(this);
+}
+
+void Vertex::destHandler(stackItem* item, uint16_t ptr){
+  // generic handler...
+  OSAP::debug("generic destHandler at " + name);
+  stackClearSlot(item);
+}
+
+void Vertex::pingRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_PINGRES;
+  payload[1] = item->data[ptr + 2];
+  // write a new gram, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 2);
+  // clear previous, 
+  stackClearSlot(item);
+  // load next... there will be one empty, as this has just arrived here... & we just wiped it 
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+void Vertex::scopeRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_SCOPERES;
+  payload[1] = item->data[ptr + 2];
+  // next items write starting here, 
+  uint16_t wptr = 2;
+  // scope time-tag, 
+  ts_writeUint32(scopeTimeTag, payload, &wptr);
+  // and read in the previous scope (this is traversal state required to delineate loops in the graph) 
+  uint16_t rptr = ptr + 3;
+  ts_readUint32(&scopeTimeTag, item->data, &rptr);
+  // write the vertex type,  
+  payload[wptr ++] = type;
+  // vport / vbus link states, 
+  if(type == VT_TYPE_VPORT){
+    payload[wptr ++] = (vport->isOpen() ? 1 : 0);
+  } else if (type == VT_TYPE_VBUS){
+    uint16_t addrSize = vbus->addrSpaceSize;
+    uint16_t addr = 0;
+    // ok we write the address size in first, then our own rxaddr, 
+    ts_writeUint16(vbus->addrSpaceSize, payload, &wptr);
+    ts_writeUint16(vbus->ownRxAddr, payload, &wptr);
+    // then *so long a we're not overwriting*, we stuff link-state bytes, 
+    while(wptr + 8 + name.length() <= VT_SLOTSIZE){
+      payload[wptr] = 0;
+      for(uint8_t b = 0; b < 8; b ++){
+        payload[wptr] |= (vbus->isOpen(addr) ? 1 : 0) << b;
+        addr ++;
+        if(addr >= addrSize) goto end;
+      }
+      wptr ++;
+    }
+    end:
+    wptr ++; // += 1 more, so we write into next, 
+  }
+  // our own indice, # siblings, and # children, 
+  ts_writeUint16(indice, payload, &wptr);
+  if(parent != nullptr){
+    ts_writeUint16(parent->numChildren, payload, &wptr);
+  } else {
+    ts_writeUint16(0, payload, &wptr);
+  }
+  ts_writeUint16(numChildren, payload, &wptr);
+  // finally, our string name:
+  ts_writeString(name, payload, &wptr);
+  // and roll that back up, rm old, and ship it, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+  stackClearSlot(item);
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+
+void Vertex::onOriginStackClear(uint8_t slot){
+  if(onOriginStackClear_cb != nullptr) return onOriginStackClear_cb(this, slot);
+}
+
+void Vertex::onDestinationStackClear(uint8_t slot){
+  if(onDestinationStackClear_cb != nullptr) return onDestinationStackClear_cb(this, slot);
+}
+
+// ---------------------------------------------- VPort Constructor and Defaults 
+
+VPort::VPort(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vp_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VPORT;
+  vport = this; 
+}
+
+// ---------------------------------------------- VBus Constructor and Defaults 
+
+VBus::VBus(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vb_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VBUS;
+  vbus = this;
+  // these should all init to nullptr, 
+  for(uint8_t ch = 0; ch < VBUS_MAX_BROADCAST_CHANNELS; ch ++){
+    broadcastChannels[ch] = nullptr;
+  }
+}
+
+void VBus::injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  // ok so first we want to see if we have anything sub'd to this channel, so
+  if(broadcastChannels[broadcastChannel] != nullptr){
+    // we have a route, so we want to load this data *as we inject some new path segments* 
+    Route* route = broadcastChannels[broadcastChannel];
+    // we could definitely do this faster w/o using the stackLoadSlot fn, but we won't do that yet... 
+    // will use the vertex-global datagram stash for that 
+    uint16_t ptr = 0; 
+    if(!findPtr(data, &ptr)){ OSAP::error("can't find ptr during broadcast injest", MEDIUM); return; }
+    // packet should look like 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <payload>
+    // we want to inject the channel's route such that 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <ch_route>, <payload>
+    // shouldn't actually be too difficult, eh?
+    // we do need to guard on lengths, 
+    if(len + route->pathLen > VT_SLOTSIZE){ OSAP::error("datagram + channel route is too large", MEDIUM); return; }
+    // copy up to PTR: pck[ptr] == PK_PTR, so we want to *include* this byte, having len ptr + 1, 
+    memcpy(datagram, data, ptr + 1);
+    // copy in route, but recall that as initialized, route->path[0] == PK_PTR, we don't want to double that up, 
+    memcpy(&(datagram[ptr + 1]), &(route->path[1]), route->pathLen - 1);
+    // then the rest of the gram, from just after-the-ptr, to end, 
+    memcpy(&datagram[ptr + 1 + route->pathLen - 1], &(data[ptr + 1]), len - ptr - 1);
+    // now we can load this in, 
+    stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len + route->pathLen - 1);
+    // aye that's it innit? 
+  }
+}
+
+void VBus::setBroadcastChannel(uint8_t channel, Route* route){
+  if(channel >= VBUS_MAX_BROADCAST_CHANNELS) return;
+  // seems a little sus, idk 
+  broadcastChannels[channel] = route;
+}
+
+void VBus::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == the key we're switching on...
+  switch(item->data[ptr + 2]){
+    case VBUS_BROADCAST_MAP_REQ:
+      // mvc request a map of our active broadcast channels, this is akin to bus link-state-scope packet
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_MAP_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // max length of channels... max 255, same as max endpoint routes (?) 
+        // this is maybe an error, consult packet spec (transport layer) for completeness, 
+        // time being... rare to have > 255 broadcast channels, 
+        payload[wptr ++] = VBUS_MAX_BROADCAST_CHANNELS;
+        // then *so long a we're not overwriting*, we stuff link-state bytes, 
+        // idk, 32 is arbitrary, we have to account for return-route length properly... 
+        uint16_t channel = 0;
+        while(wptr + 32 <= VT_SLOTSIZE){
+          payload[wptr] = 0;
+          for(uint8_t b = 0; b < 8; b ++){
+            payload[wptr] |= (broadcastChannels[channel] == nullptr ? 0 : 1) << b;
+            channel ++;
+            if(channel >= VBUS_MAX_BROADCAST_CHANNELS) goto end;
+          }
+          wptr ++;
+        }
+        end:
+        wptr ++; // += 1 more, so we write into next, 
+        // we're ready to write the reply back, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_QUERY_REQ:
+      // mvc requests broadcast channel info on a particular channel, 
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_QUERY_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // the indice of the channel we're looking at, 
+        uint16_t ch = item->data[ptr + 4];
+        // if the ch exists, 
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS && broadcastChannels[ch] != nullptr){
+          payload[wptr ++] = 1;
+          // now... these are route objects, but we only use the path part... 
+          // but we'll re-use route-object serialization schemes from EP_ROUTE_QUERY_REQ 
+          ts_writeUint16(broadcastChannels[ch]->ttl, payload, &wptr);
+          ts_writeUint16(broadcastChannels[ch]->segSize, payload, &wptr);
+          // path copy 
+          memcpy(&(payload[wptr]), broadcastChannels[ch]->path, broadcastChannels[ch]->pathLen);
+          wptr += broadcastChannels[ch]->pathLen;
+        } else {
+          payload[wptr ++] = 0;
+        }
+        // write reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_SET_REQ:
+      // mvc requests to set a broadcast channel route 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // ch to write into...
+        uint8_t ch = item->data[ptr + 4];
+        // reply-write-pointer 
+        uint16_t wptr = 0;
+        // prep a response, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_SET_RES;
+        payload[wptr ++] = id;
+        if(ch >= VBUS_MAX_BROADCAST_CHANNELS){
+          // won't go 
+          OSAP::error("attempt to write to oob broadcast channel");
+          payload[wptr ++] = 0;
+        } else {
+          // should go 
+          payload[wptr ++] = 1;          
+          if(broadcastChannels[ch] != nullptr) OSAP::debug("overwriting previous broadcast ch at " + String(ch));
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          setBroadcastChannel(ch, new Route(path, pathLen, ttl, segSize));
+        }
+        // in any case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_RM_REQ:
+      // mvc requests to rm a broadcast channel, 
+      // todo / cleanliness: might be salient to 'write 0' to delete (?) who knows 
+      {
+        // id & indice to rm 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t ch = item->data[ptr + 4];
+        uint16_t wptr = 0;
+        // prep res 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_RM_RES;
+        payload[wptr ++] = id;
+        // can we rm ?
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS){
+          if(broadcastChannels[ch] != nullptr) {
+            delete broadcastChannels[ch];
+            broadcastChannels[ch] = nullptr;
+            payload[wptr ++] = 1;
+          } else {
+            // didn't exist, so, a bad delete: 
+            payload[wptr ++] = 0;
+          }
+        } else {
+          // bad req, should throw errors... 
+          payload[wptr ++] = 0;
+        }
+        // can send now, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    default:
+      OSAP::error("vbus rx msg w/ unrecognized vbus key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/core/vertex.h b/system/firmware/lpf-filament-sensor/src/osape/core/vertex.h
new file mode 100644
index 0000000000000000000000000000000000000000..842d5733f64fa6661165c84e2193b2c0604892d1
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/core/vertex.h
@@ -0,0 +1,131 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "routes.h"
+#include "stack.h"
+// vertex config is build dependent, define in <folder-containing-osape>/osapConfig.h 
+#include "./osap_config.h" 
+
+// we have the vertex type, 
+// since it contains ptrs to others of its type, we fwd declare the type...
+class Vertex;
+// ... 
+typedef struct stackItem stackItem;
+typedef struct VPort VPort;
+typedef struct VBus VBus;
+
+// default vt fns 
+void vtLoopDefault(Vertex* vt);
+void vtOnOriginStackClearDefault(Vertex* vt, uint8_t slot);
+void vtOnDestinationStackClearDefault(Vertex* vt, uint8_t slot);
+
+// addressable node in the graph ! 
+class Vertex {
+  public:
+    // just temporary stashes, used all over the place to prep messages... 
+    static uint8_t payload[VT_SLOTSIZE];
+    static uint8_t datagram[VT_SLOTSIZE];
+    // -------------------------------- FN PTRS 
+    // these are *genuine function ptrs* not member functions, my dudes 
+    void (*loop_cb)(Vertex* vt) = nullptr;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    void (*onDestinationStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    // -------------------------------- Methods
+    virtual void loop(void);
+    virtual void destHandler(stackItem* item, uint16_t ptr);
+    void pingRequestHandler(stackItem* item, uint16_t ptr);
+    void scopeRequestHandler(stackItem* item, uint16_t ptr);
+    virtual void onOriginStackClear(uint8_t slot);
+    virtual void onDestinationStackClear(uint8_t slot);
+    // -------------------------------- DATA
+    // a type, a position, a name 
+    uint8_t type = VT_TYPE_CODE;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we were last scoped (need for graph traversals, final implementation tbd)
+    uint32_t scopeTimeTag = 0;
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    Vertex* parent = nullptr;
+    Vertex* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // sometimes a vertex is a vport, sometimes it is a vbus, 
+    VPort* vport;
+    VBus* vbus;
+    // -------------------------------- CONSTRUCTORS 
+    Vertex( 
+      Vertex* _parent, 
+      String _name, 
+      void (*_loop)(Vertex* vt),
+      void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+      void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+    );
+    Vertex(Vertex* _parent, String _name) : Vertex(_parent, _name, nullptr, nullptr, nullptr){};
+    Vertex(String _name) : Vertex(nullptr, _name, nullptr, nullptr, nullptr){};
+};
+
+// ---------------------------------------------- VPort 
+
+class VPort : public Vertex {
+  public:
+    // -------------------------------- OK these bbs are methods, 
+    virtual void send(uint8_t* data, uint16_t len) = 0;
+    virtual boolean cts(void) = 0;
+    virtual boolean isOpen(void) = 0;
+    // base constructor, 
+    VPort(Vertex* _parent, String _name);
+};
+
+// ---------------------------------------------- VBus 
+
+class VBus : public Vertex{
+  public:
+    // -------------------------------- Methods: these are purely virtual... 
+    virtual void send(uint8_t* data, uint16_t len, uint8_t rxAddr) = 0;
+    virtual void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) = 0;
+    // clear to send, clear to broadcast, 
+    virtual boolean cts(uint8_t rxAddr) = 0;
+    virtual boolean ctb(uint8_t broadcastChannel) = 0;
+    // link state per rx-addr,
+    virtual boolean isOpen(uint8_t rxAddr) = 0;
+    // handle things aimed at us, for mvc etc 
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // busses can read-in to broadcasts,
+    void injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel);
+    // we have also... broadcast channels... these are little route stubs & channel pairs, which we just straight up index, 
+    Route* broadcastChannels[VBUS_MAX_BROADCAST_CHANNELS];
+    // have to update those... 
+    void setBroadcastChannel(uint8_t channel, Route* route);
+    // has an rx addr, 
+    uint16_t ownRxAddr = 0;
+    // has a width-of-addr-space, 
+    uint16_t addrSpaceSize = 0;
+    // base constructor, children inherit... 
+    VBus(Vertex* _parent, String _name);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.cpp b/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81cc05bb3b38d85273a838a4b05df31bff2783a9
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.h b/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.h
new file mode 100644
index 0000000000000000000000000000000000000000..b47070ca26d021f113da680a6835df65712d4007
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.cpp b/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d9fe310be794e69ef9040e2ee33a26bcf986f9
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.cpp
@@ -0,0 +1,351 @@
+/*
+osape/vertices/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "../core/osap.h"
+#include "../core/packets.h"
+
+// -------------------------------------------------------- Constructors 
+
+// route constructor 
+EndpointRoute::EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+  if(_mode != EP_ROUTEMODE_ACKED && _mode != EP_ROUTEMODE_ACKLESS){
+    _mode = EP_ROUTEMODE_ACKLESS;
+  }
+  route = _route;
+  ackMode = _mode;
+  timeoutLength = _timeoutLength;
+}
+
+EndpointRoute::~EndpointRoute(void){
+  delete route;
+}
+
+// base constructor, 
+Endpoint::Endpoint(
+  Vertex* _parent, String _name, 
+  EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+  boolean (*_beforeQuery)(void)
+) : Vertex(_parent, "ep_" + _name) {
+  // type, 
+	type = VT_TYPE_ENDPOINT;
+  // set callbacks,
+  if(_onData) onData_cb = _onData;
+  if(_beforeQuery) beforeQuery_cb = _beforeQuery;
+}
+
+// -------------------------------------------------------- Dummies / Defaults 
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+// -------------------------------------------------------- Endpoint Route / Write API 
+
+void Endpoint::write(uint8_t* _data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(data, _data, len);
+  dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state == EP_TX_AWAITING_ACK){
+      routes[r]->state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      routes[r]->state = EP_TX_FRESH;
+    }
+  }
+}
+
+// add a route to an endpoint, returns indice where it's dropped, 
+uint8_t Endpoint::addRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+	// guard against more-than-allowed routes 
+	if(numRoutes >= ENDPOINT_MAX_ROUTES) {
+    OSAP::error("route add is oob", MEDIUM); 
+    return 0;
+	}
+  // build, stash, increment 
+  uint8_t indice = numRoutes;
+  routes[numRoutes ++] = new EndpointRoute(_route, _mode, _timeoutLength);
+  return indice; 
+}
+
+boolean Endpoint::clearToWrite(void){
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+// -------------------------------------------------------- Loop 
+
+void Endpoint::loop(void){
+  // ok we are doing a time-based dispatch... 
+  unsigned long now = millis();
+  EndpointRoute* routeTxList[ENDPOINT_MAX_ROUTES];
+  uint8_t numTxRoutes = 0;
+  // stack fresh routes, and also transition timeouts / etc, 
+  // we make & sort this list, but set it up round-robin, since many 
+  // cases will see the same TTL & same write-to time, meaning routes that 
+  // happen to be in low indices would chance on "higher priority" 
+  uint8_t r = lastRouteServiced;
+  for(uint8_t i = 0; i < numRoutes; i ++){
+    r ++; if(r >= numRoutes) r = 0;
+    switch(routes[r]->state){
+      case EP_TX_FRESH:
+        routeTxList[numTxRoutes ++] = routes[r];
+        break;
+      case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_FRESH;
+        }
+      default:
+        // noop for IDLE / otherwise...
+        break;
+    }
+  }
+  // now, would do a sort... they're all fresh at the same time, so lowest TTL would win,
+  // this one we would want to be stable, meaning original order is preserved in 
+  // otherwise identical cases, since we round-robin fairness as well as TTL / TTD  
+  #warning no sort algo yet, 
+  // serve 'em... these are all EP_TX_FRESH state, 
+  for(r = 0; r < numTxRoutes; r ++){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // make sure we'll have enough space...
+      if(dataLen + routeTxList[r]->route->pathLen + 3 >= VT_SLOTSIZE){
+        OSAP::error("attempting to write oversized datagram at " + name, MEDIUM);
+        routeTxList[r]->state = EP_TX_IDLE;
+        continue;
+      }
+      // write dest key, mode key, & id if acked, 
+      uint16_t wptr = 0;
+      payload[wptr ++] = PK_DEST;
+      if(routeTxList[r]->ackMode == EP_ROUTEMODE_ACKLESS){
+        payload[wptr ++] = EP_SS_ACKLESS;
+      } else {
+        payload[wptr ++] = EP_SS_ACKED;
+        payload[wptr ++] = nextAckID;
+        routeTxList[r]->ackId = nextAckID;
+        nextAckID ++;
+      } 
+      // write data into the payload, 
+      memcpy(&(payload[wptr]), data, dataLen);
+      wptr += dataLen;
+      // write the packet, 
+      uint16_t len = writeDatagram(datagram, VT_SLOTSIZE, routeTxList[r]->route, payload, wptr);
+      // tx time is now, and state is awaiting ack, 
+      routeTxList[r]->lastTxTime = now;
+      routeTxList[r]->state = EP_TX_AWAITING_ACK;
+      lastRouteServiced = r;
+      // ingest it...
+      stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len);
+    } else {
+      // stack has no more empty slots, bail from the loop, 
+      break;
+    }
+  } // end fresh-tx-awaiting state checks, 
+}
+
+// -------------------------------------------------------- Destination Handler  
+
+void Endpoint::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == EP_KEY, ptr + 3 = ID (if ack req.) 
+  switch(item->data[ptr + 2]){
+    case EP_SS_ACKLESS:
+      { // singlesegment transmit-to-us, w/o ack, 
+        uint8_t* rxData = &(item->data[ptr + 3]); uint16_t rxLen = item->len - (ptr + 4);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+        switch(resp){
+          case EP_ONDATA_WAIT:    // in a wait case, we no-op / escape, it comes back around 
+            item->arrivalTime = millis();
+            break;
+          case EP_ONDATA_ACCEPT:  // here we copy it in, but carry on to the reject term to delete og gram
+            memcpy(data, rxData, rxLen);
+            dataLen = rxLen;
+          case EP_ONDATA_REJECT:  // here we simply reject it, 
+            stackClearSlot(item);
+            break;
+        } // end resp-handler, 
+      }
+      break;
+    case EP_SS_ACKED:
+      { // singlesegment transmit-to-us, w/ ack, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t* rxData = &(item->data[ptr + 4]); uint16_t rxLen = item->len - (ptr + 5);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+          switch(resp){
+            case EP_ONDATA_WAIT: // this is a little danger-danger, 
+              item->arrivalTime = millis();
+              break;
+            case EP_ONDATA_ACCEPT:
+              memcpy(data, rxData, rxLen);
+              dataLen = rxLen;
+            case EP_ONDATA_REJECT:
+              // write the ack, ship it, 
+              payload[0] = PK_DEST;
+              payload[1] = EP_SS_ACK;
+              payload[2] = id;
+              uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 3);
+              stackClearSlot(item);
+              stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+              break;
+          }
+      }
+      break;
+    case EP_QUERY:
+      {
+        // beforeQuery, 
+        beforeQuery_cb();
+        // request for our data, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_QUERY_RESP;
+        payload[2] = item->data[ptr + 3];
+        memcpy(&(payload[3]), data, dataLen);
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, dataLen + 3);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_SS_ACK:
+      // acks to us, 
+      for(uint8_t r = 0; r < numRoutes; r ++){
+        if(item->data[ptr + 3] == routes[r]->ackId){
+          switch(routes[r]->state){
+            case EP_TX_AWAITING_ACK:
+              routes[r]->state = EP_TX_IDLE;
+              goto ackEnd;
+            case EP_TX_AWAITING_AND_FRESH:
+              routes[r]->state = EP_TX_FRESH;
+              goto ackEnd;
+            case EP_TX_FRESH:
+            case EP_TX_IDLE:
+            default:
+              // these are nonsense states, likely double-transmits, likely safely ignored,
+              goto ackEnd;
+          } // end switch 
+        }
+      } // end for-each route, if we've reached this point, still dump it;
+      ackEnd:
+      stackClearSlot(item);
+      break;
+    case EP_ROUTE_QUERY_REQ:
+      // MVC request for a route of ours, 
+      {
+        uint8_t id = item->data[ptr + 3];
+        uint16_t r = ts_readUint16(item->data, ptr + 4);
+        uint16_t wptr = 0;
+        // dest, key, id... mode, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_QUERY_RES;
+        payload[wptr ++] = id;
+        if(r < numRoutes){
+          payload[wptr ++] = routes[r]->ackMode;
+          // ttl, segsize, 
+          ts_writeUint16(routes[r]->route->ttl, payload, &wptr);
+          ts_writeUint16(routes[r]->route->segSize, payload, &wptr);
+          // path ! 
+          memcpy(&(payload[wptr]), routes[r]->route->path, routes[r]->route->pathLen);
+          wptr += routes[r]->route->pathLen;
+        } else {
+          payload[wptr ++] = 0; // no-route-here, 
+        }
+        // clear request, write reply in place, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_SET_REQ:
+      // MVC request to set a new route, 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_SET_RES;
+        payload[2] = id;
+        if(numRoutes + 1 <= ENDPOINT_MAX_ROUTES){
+          // tell call-er it should work, 
+          payload[3] = 1;
+          // gather & set route, 
+          uint8_t mode = item->data[ptr + 4];
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          OSAP::debug("adding path... w/ ttl " + String(ttl) + " ss " + String(segSize) + " pathLen " + String(pathLen));
+          uint8_t routeIndice = addRoute(new Route(path, pathLen, ttl, segSize), mode);
+          payload[4] = routeIndice;
+        } else {
+          // nope, 
+          payload[3] = 0;
+          payload[4] = 0;
+        }
+        // either case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 5);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_RM_REQ:
+      // MVC request to rm a route... 
+      {
+        // msg id, & indice to remove, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t r = item->data[ptr + 4];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_RM_RES;
+        payload[2] = id;
+        if(r < numRoutes){
+          // RM ok, 
+          payload[3] = 1;
+          // delete / run destructor 
+          delete routes[r];
+          // shift...
+          for(uint8_t i = r; i < numRoutes - 1; i ++){
+            routes[i] = routes[i + 1];
+          }
+          // last is null, 
+          routes[numRoutes] = nullptr;
+          numRoutes --;
+        } else {
+          // rm not-ok
+          payload[3] = 0;
+        }
+        // either case, write reply 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 4);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    default:
+      OSAP::error("endpoint rx msg w/ unrecognized endpoint key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } // end switch... 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.h b/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.h
new file mode 100644
index 0000000000000000000000000000000000000000..b14e45a64f1346b4e034d853343a336fd75c59aa
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape/vertices/endpoint.h
@@ -0,0 +1,98 @@
+/*
+osap/vertices/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "../core/vertex.h"
+#include "../core/packets.h"
+
+// ---------------------------------------------- Endpoint Routes, extends OSAP Core Routes 
+
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+class EndpointRoute {
+  public: 
+    Route* route;
+    uint8_t ackId = 0;
+    uint8_t ackMode = EP_ROUTEMODE_ACKLESS;
+    EP_ROUTE_STATES state = EP_TX_IDLE;
+    uint32_t lastTxTime = 0;
+    uint32_t timeoutLength;
+    // constructor, 
+    EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength = 1000);
+    // destructor...
+    ~EndpointRoute(void);
+};
+
+// ---------------------------------------------- Endpoints 
+
+// endpoint handler responses must be one of these enum - 
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+// default handlers, 
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len);
+boolean beforeQueryDefault(void);
+
+class Endpoint : public Vertex {
+  public:
+    // local data store & length, 
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t dataLen = 0; 
+    // callbacks: on new data & before a query is written out 
+    EP_ONDATA_RESPONSES (*onData_cb)(uint8_t* data, uint16_t len) = onDataDefault;
+    boolean (*beforeQuery_cb)(void) = beforeQueryDefault;
+    // we override vertex loop, 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // methods,
+    void write(uint8_t* _data, uint16_t len);
+    boolean clearToWrite(void);
+    uint8_t addRoute(Route* _route, uint8_t _mode = EP_ROUTEMODE_ACKLESS, uint32_t _timeoutLength = 1000);
+    // routes, for tx-ing to:
+    EndpointRoute* routes[ENDPOINT_MAX_ROUTES];
+    uint16_t numRoutes = 0;
+    uint16_t lastRouteServiced = 0;
+    uint8_t nextAckID = 77;
+    // base constructor, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+      boolean (*_beforeQuery)(void)
+    );
+    // these are called "delegating constructors" ... best reference is 
+    // here: https://en.cppreference.com/w/cpp/language/constructor 
+    // onData only, 
+    Endpoint(   
+      Vertex* _parent, String _name,
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len)
+    ) : Endpoint ( 
+      _parent, _name, _onData, nullptr
+    ){};
+    // beforeQuery only, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      boolean (*_beforeQuery)(void)
+    ) : Endpoint (
+      _parent, _name, nullptr, _beforeQuery
+    ){};
+    // name only, 
+    Endpoint(   
+      Vertex* _parent, String _name
+    ) : Endpoint (
+      _parent, _name, nullptr, nullptr
+    ){};
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/LICENSE.md b/system/firmware/lpf-filament-sensor/src/osape_arduino/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/README.md b/system/firmware/lpf-filament-sensor/src/osape_arduino/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da4c90cb6b618b1b8206b0ddf40a240acbaa4ca7
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/README.md
@@ -0,0 +1,7 @@
+## OSAP Arduino
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+It does not do anything on its own; this one builds helper classes to turn Arduino `Serial` and `Wire` objects into *virtual ports* and *virtual busses* respectively. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.cpp b/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8634694ff54bf2f45fdc704f3fd961168f4620bb
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.cpp
@@ -0,0 +1,77 @@
+/*
+arduino-ports/vp_arduinoWire.cpp
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#include "vb_arduinoWire.h"
+
+// static stash: same per instance, 
+uint8_t stash[32];
+uint8_t stashLen = 0;
+
+VBus_ArduinoWire::VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr
+) : VBus ( _parent, _name ) {
+  wire = _wire;
+  ownRxAddr = _ownRxAddr;
+}
+
+void VBus_ArduinoWire::begin(void){
+  wire->begin(ownRxAddr);
+  wire->onReceive(this->onRecieve);
+}
+
+void VBus_ArduinoWire::onRecieve(int count){
+  Wire.readBytes(stash, count);
+  stashLen = count;
+}
+
+void VBus_ArduinoWire::loop(void){
+  // check incoming, 
+  if(stashLen > 0){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      stackLoadSlot(this, VT_STACK_ORIGIN, stash, stashLen);
+    }
+    stashLen = 0;
+  }
+}
+
+void VBus_ArduinoWire::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  digitalWrite(A1, HIGH);
+  // this'll be the big hangup, 
+  if(len > 32) return;
+  // this might guard, if we are already rx'ing... 
+  if(wire->available()) return;
+  // become host, 
+  wire->end();
+  wire->begin();
+  // transmit, 
+  wire->beginTransmission(rxAddr);
+  wire->write(data, len);
+  uint8_t res = wire->endTransmission();
+  // become guest again, 
+  wire->end();
+  wire->begin(ownRxAddr);
+  // check, 
+  //if(res != 0) 
+  // DEBUG("res " + String(res) + " txd " + String(len));
+  digitalWrite(A1, LOW);
+}
+
+boolean VBus_ArduinoWire::cts(uint8_t rxAddr){
+  return true;
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.h b/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.h
new file mode 100644
index 0000000000000000000000000000000000000000..b098634545544070b65a46e1106719567f2fbe5b
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/vb_arduinoWire.h
@@ -0,0 +1,43 @@
+/*
+arduino-ports/vp_arduinoWire.h
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#ifndef ARDU_WIRELINK_H_
+#define ARDU_WIRELINK_H_
+
+#include <Arduino.h>
+#include <Wire.h>
+#include "../osape/core/vertex.h"
+
+#define WIRELINK_BUFSIZE 255 
+
+class VBus_ArduinoWire : public VBus {
+  public:
+    void begin(void);
+    // -------------------------------- our own loop, cts, and send... 
+    void loop(void) override; 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override; 
+    boolean cts(uint8_t rxAddr) override; 
+    // -------------------------------- data 
+    TwoWire* wire;
+    static void onRecieve(int count);
+    // -------------------------------- constructors
+    VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr);
+};
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.cpp b/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f71fe57592eccba322baf9108b38d068a2aed544
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.cpp
@@ -0,0 +1,174 @@
+/*
+arduino-ports/ardu-vport.h
+
+turns serial objects into competent link layers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "vp_arduinoSerial.h"
+#include "./osape/utils/cobs.h"
+#include "../osape/core/osap.h"
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Uart* _uart
+) : VPort ( _parent, _name ){
+  stream = _uart; // should convert Uart* to Stream*, as Uart inherits stream 
+  uart = _uart; 
+}
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Serial_* _usbcdc
+) : VPort ( _parent, _name ){
+  stream = _usbcdc;
+  usbcdc = _usbcdc;
+}
+
+void VPort_ArduinoSerial::begin(uint32_t baudRate){
+  if(uart != nullptr){
+    uart->begin(baudRate);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(baudRate); 
+  }
+}
+
+void VPort_ArduinoSerial::begin(void){
+  if(uart != nullptr){
+    uart->begin(1000000);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(9600);  // baud ignored on cdc begin  
+  }
+}
+
+// link packets are max 256 bytes in length, including the 0 delimiter 
+// structured like:
+// checksum | pck/ack key | pck id | cobs encoded data | 0 
+
+void VPort_ArduinoSerial::loop(void){
+  // byte injestion: think of this like the rx interrupt stage, 
+  while(stream->available()){
+    // read byte into the current stub, 
+    rxBuffer[rxBufferWp ++] = stream->read();
+    if(rxBuffer[rxBufferWp - 1] == 0){
+      // always reset keepalive last-rx time, 
+      lastRxTime = millis();
+      // 1st, we checksum:
+      if(rxBuffer[0] != rxBufferWp){ 
+        OSAP::error("serLink bad checksum, cs: " + String(rxBuffer[0]) + " wp: " + String(rxBufferWp), MINOR);
+      } else {
+        // acks, packs, or broken things 
+        switch(rxBuffer[1]){
+          case SERLINK_KEY_PCK:
+            // dirty guard for retransmitted packets, 
+            if(rxBuffer[2] != lastIdRxd){
+              inAwaitingId = rxBuffer[2]; // stash ID 
+              inAwaitingLen = cobsDecode(&(rxBuffer[3]), rxBufferWp - 2, inAwaiting); // fill inAwaiting 
+            } else {
+              OSAP::error("serLink double rx", MINOR);
+            }
+            break;
+          case SERLINK_KEY_ACK:
+            if(rxBuffer[2] == outAwaitingId){
+              outAwaitingLen = 0;
+            }
+            break;
+          case SERLINK_KEY_KEEPALIVE:
+            // noop, 
+            break;
+          default:
+            // makes no sense, 
+            break;
+        }
+      }
+      // always reset on delimiter, 
+      rxBufferWp = 0;
+    }
+  } // end while-receive 
+
+  // check insertion & genny the ack if we can 
+  if(inAwaitingLen && stackEmptySlot(this, VT_STACK_ORIGIN) && !ackIsAwaiting){
+    stackLoadSlot(this, VT_STACK_ORIGIN, inAwaiting, inAwaitingLen);
+    ackIsAwaiting = true;
+    ackAwaiting[0] = 4;                 // checksum still, innit 
+    ackAwaiting[1] = SERLINK_KEY_ACK;   // it's an ack bruv 
+    ackAwaiting[2] = inAwaitingId;      // which pck r we akkin m8 
+    ackAwaiting[3] = 0;                 // delimiter 
+    inAwaitingLen = 0;
+  }
+
+  // check & execute actual tx 
+  checkOutputStates();
+}
+
+void VPort_ArduinoSerial::send(uint8_t* data, uint16_t len){
+  //digitalWrite(A4, !digitalRead(A4));
+  // double guard?
+  if(!cts()) return;
+  // setup, 
+  outAwaiting[0] = len + 5;               // pck[0] is checksum = len + checksum + cobs start + cobs delimit + ack/pack + id 
+  outAwaiting[1] = SERLINK_KEY_PCK;       // this ones a packet m8 
+  outAwaitingId ++; if(outAwaitingId == 0) outAwaitingId = 1;
+  outAwaiting[2] = outAwaitingId;         // an id     
+  cobsEncode(data, len, &(outAwaiting[3]));  // encode 
+  outAwaiting[len + 4] = 0;               // stuff delimiter, 
+  outAwaitingLen = outAwaiting[0];        // track... 
+  // transmit attempts etc 
+  outAwaitingNTA = 0;
+  outAwaitingLTAT = 0;
+  // try it 
+  checkOutputStates();                    // try / start write 
+}
+
+// we are CTS if outPck is not occupied, 
+boolean VPort_ArduinoSerial::cts(void){
+  return (outAwaitingLen == 0);
+}
+
+// we are open if we've heard back lately, 
+boolean VPort_ArduinoSerial::isOpen(void){
+  return (millis() - lastRxTime < SERLINK_KEEPALIVE_RX_TIME && lastRxTime != 0);
+}
+
+void VPort_ArduinoSerial::checkOutputStates(void){
+  if(ackIsAwaiting && txBufferLen == 0){   // can we ack? 
+    memcpy(txBuffer, ackAwaiting, 4);
+    txBufferLen = 4;
+    lastTxTime = millis();
+    txBufferRp = 0;
+    ackIsAwaiting = false;
+  } else if(outAwaitingLen > 0 && txBufferLen == 0){   // would we be clear to tx ? 
+    // check retransmit cases, 
+    if(outAwaitingLTAT == 0 || outAwaitingLTAT + SERLINK_RETRY_TIME < micros()){
+      memcpy(txBuffer, outAwaiting, outAwaitingLen);
+      outAwaitingLTAT = micros();
+      txBufferLen = outAwaitingLen;
+      lastTxTime = millis();
+      txBufferRp = 0;
+      outAwaitingNTA ++;
+    } 
+    // check if last attempt, 
+    if(outAwaitingNTA >= SERLINK_RETRY_MACOUNT){
+      outAwaitingLen = 0;
+    }
+  } else if (millis() - lastTxTime > SERLINK_KEEPALIVE_TX_TIME && txBufferLen == 0){
+    //OSAP::debug("keepalive-ing " + name + " " + String(isOpen()));
+    memcpy(txBuffer, keepAlivePacket, 3);
+    txBufferLen = 3;
+    lastTxTime = millis();
+  }
+  // finally, we write out so long as we can: 
+  // we aren't guaranteed to get whole pckts out in each fn call 
+  while(stream->availableForWrite() && txBufferLen != 0){
+    // output next byte, 
+    stream->write(txBuffer[txBufferRp ++]);
+    // check for end of buffer; reset transmit states if so 
+    if(txBufferRp >= txBufferLen) {
+      txBufferLen = 0; 
+      txBufferRp = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.h b/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa518aabc7e8905a85abf8ec07d4a2138b2f10f2
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_arduino/vp_arduinoSerial.h
@@ -0,0 +1,88 @@
+/*
+arduino-ports/vp_arduinoSerial.h
+
+turns arduino serial objects into competent link layers, for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ARDU_SERLINK_H_
+#define ARDU_SERLINK_H_
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+#define SERLINK_BUFSIZE 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+#define SERLINK_SEGSIZE SERLINK_BUFSIZE - 5
+// packet keys; 
+#define SERLINK_KEY_PCK 170  // 0b10101010
+#define SERLINK_KEY_ACK 171  // 0b10101011
+#define SERLINK_KEY_KEEPALIVE 173 
+// retry settings 
+#define SERLINK_RETRY_MACOUNT 2
+#define SERLINK_RETRY_TIME 100000  // microseconds 
+#define SERLINK_KEEPALIVE_TX_TIME 800 // milliseconds 
+#define SERLINK_KEEPALIVE_RX_TIME 1200 // ms 
+
+#define SERLINK_LIGHT_ON_TIME 100 // in ms 
+
+// note that we use uint8_t write ptrs / etc: and a size of 255, 
+// so we are never dealing w/ wraps etc, god bless 
+
+class VPort_ArduinoSerial : public VPort {
+  public:
+    // arduino std begin 
+    void begin(uint32_t baud);
+    void begin(void);
+    // -------------------------------- our own gd send & cts & loop fns, 
+    void loop(void) override;
+    void checkOutputStates(void);
+    void send(uint8_t* data, uint16_t len) override;
+    boolean cts(void) override;
+    boolean isOpen(void) override;
+    // -------------------------------- Data 
+    // Uart & USB are both Stream classes, 
+    Stream* stream;
+    // we have an overloaded constructor w/ uart or Serial_, the usb class 
+    Uart* uart = nullptr;
+    Serial_* usbcdc = nullptr; 
+    // incoming, always kept clear to receive: 
+    uint8_t rxBuffer[SERLINK_BUFSIZE];
+    uint8_t rxBufferWp = 0;
+    // keepalive state, 
+    uint32_t lastRxTime = 0;
+    uint32_t lastTxTime = 0;
+    uint8_t keepAlivePacket[3] = {3, SERLINK_KEY_KEEPALIVE, 0};
+    // guard on double transmits 
+    uint8_t lastIdRxd = 0;
+    // incoming stash
+    uint8_t inAwaiting[SERLINK_BUFSIZE];
+    uint8_t inAwaitingId = 0;
+    uint8_t inAwaitingLen = 0;
+    // outgoing ack, 
+    uint8_t ackAwaiting[4];
+    boolean ackIsAwaiting = false;
+    // outgoing await,
+    uint8_t outAwaiting[SERLINK_BUFSIZE];
+    uint8_t outAwaitingId = 1;
+    uint8_t outAwaitingLen = 0;
+    uint8_t outAwaitingNTA = 0;
+    unsigned long outAwaitingLTAT = 0;
+    // outgoing buffer,
+    uint8_t txBuffer[SERLINK_BUFSIZE];
+    uint8_t txBufferLen = 0;
+    uint8_t txBufferRp = 0;
+    // -------------------------------- Constructors 
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Uart* _uart);
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Serial_* _usbcdc);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/README.md b/system/firmware/lpf-filament-sensor/src/osape_ucbus/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e5a9fae5795a46730372cd9533efa958bc12c2e
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/README.md
@@ -0,0 +1,6 @@
+## UART-Clocked Bus Submodule 
+
+https://gitlab.cba.mit.edu/jakeread/ucbus 
+https://github.com/jakeread/ucbus 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.cpp b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2bd443fa0154b4e62e4392611b7afabb257fb
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.cpp
@@ -0,0 +1,510 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "ucBusDipConfig.h"
+#include "../indicators.h"
+#include "../osape/core/osap.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+// last-time-rx'd 
+volatile uint32_t lastRxTime = 0;
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts: not the highest priority (0), just under. 
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  NVIC_SetPriority(SERCOM1_IRQn, 1);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // track keepalive 
+    lastRxTime = millis();
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { 
+    OSAP::error("ucbus-drop outsize numToken rx", MINOR); 
+    return; 
+  }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    OSAP::error("ucbus-drop rx overfull buffer", MINOR);
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || rxCh == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        OSAP::error("ucbus-drop rx FC fails on ch " + String(rxCh), MINOR);
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        // ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        // inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+boolean ucBusDrop_ctrA(void){
+  // likewise
+  return (inBufferLen[0] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+size_t ucBusDrop_readA(uint8_t* dest){
+  if(!ucBusDrop_ctrA()) return 0;
+  // we read out the whole gd thing,
+  size_t len = inBufferLen[0];
+  memcpy(dest, &(inBuffer[0]), len);
+  inBufferLen[0] = 0; // now empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusDrop_isPresent(uint8_t drop){
+  // can't tx anywhere other than to head, 
+  if(drop > 0) return false;
+  return (millis() - lastRxTime < UB_KEEPALIVE_TIME);
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..281f430bd6ced5264fd9607ce8f51a1a9c31cbab
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusDrop.h
@@ -0,0 +1,51 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t* dest);
+boolean ucBusDrop_ctrA(void);
+size_t ucBusDrop_readA(uint8_t* dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+boolean ucBusDrop_isPresent(uint8_t rxAddr);
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.cpp b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..854e488395920dd19b812643282ddbf9c7f3ae25
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.cpp
@@ -0,0 +1,386 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../osape/core/osap.h"
+#include "./utils_samd51/peripheral_nums.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// last-rx'd-time, per drop presence-detect, 
+volatile uint32_t lastRxTime[UB_MAX_DROPS];
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin, also set lastRxTime to zero for each, 
+  for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+    lastRxTime[d] = 0;
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      outBufferLen[ch] = 0;
+      outBufferRp[ch] = 0;
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (channel '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] || ch == 0)){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update keepalive: last we heard from this drop:
+    lastRxTime[rxDrop] = millis();
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { 
+      OSAP::error("ucbus-head outsize numToken rx", MEDIUM); 
+      return; 
+    }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ 
+      OSAP::error("ucbus-head rx FC broken", MEDIUM); 
+      return; 
+    }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      OSAP::error("ucbus-head rx packet too-long", MEDIUM);
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero / are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusHead_isPresent(uint8_t drop){
+  if(drop > UCBUS_MAX_DROPS) return false;
+  return (millis() - lastRxTime[drop] < UB_KEEPALIVE_TIME);
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: channel ID
+  outBuffer[0][0] = channel;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..65f43edcfc482f9656fe30d0bf7f7ea0f9c1eb67
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+boolean ucBusHead_isPresent(uint8_t drop); // have we heard from this drop recently ? 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusMacros.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusMacros.h
new file mode 100644
index 0000000000000000000000000000000000000000..72f3f0c0b60e6c7efac390db2e6db4be7e9b133a
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucBusMacros.h
@@ -0,0 +1,127 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "./ucbus_config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// time-until-considered-dead, in ms  
+#define UB_KEEPALIVE_TIME 200 
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL 2 // A: 0, B: 1, C: 2
+#define UB_RXPERIPHERAL 2
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.cpp b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08742fdc5cda9435f8ad54b76a4f85c81c433b1e
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..97ec2b5750e86bbd6d98acd3ef42c02b489240f4
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.cpp b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a49901b40751839e972e4f5d778179834dba1868
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.cpp
@@ -0,0 +1,95 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusDrop.h"
+#include "../osape/core/osap.h"
+
+// badness, direct write in future 
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusDrop::VBus_UCBusDrop(Vertex* _parent, String _name
+): VBus(_parent, _name){
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusDrop::begin(void){
+  ucBusDrop_setup(true, 0);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::begin(uint8_t _ownRxAddr){
+  ucBusDrop_setup(false, _ownRxAddr);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::loop(void){
+  // can we shift-in from channel a / broadcast messages ?
+  // also... stack 'em from the broadcast channel first, typically higher priority 
+  if(ucBusDrop_ctrA()){
+    // and if we have an empty space... 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+    // get len & strip out the broadcastChannel, which was stuffed at [0]
+    uint16_t len = ucBusDrop_readA(_tempBuffer);
+    injestBroadcastPacket(&(_tempBuffer[1]), len - 1, _tempBuffer[0]);
+    }
+  }
+  // can we shift-in from channel b / directed messages ? 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+void VBus_UCBusDrop::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    OSAP::error("ubd tx while not clear", MEDIUM);
+  }
+}
+
+boolean VBus_UCBusDrop::cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  return (rxAddr == 0 && ucBusDrop_ctsB());
+}
+
+void VBus_UCBusDrop::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  OSAP::debug("Broadcast is unwritten");
+}
+
+boolean VBus_UCBusDrop::ctb(uint8_t broadcastChannel){
+  OSAP::debug("Bus Drop CTB is unwritten");
+  return false;
+}
+
+boolean VBus_UCBusDrop::isOpen(uint8_t rxAddr){
+  return ucBusDrop_isPresent(rxAddr);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4333e6491b0439d01ae4bc480bce37af864f5
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusDrop.h
@@ -0,0 +1,41 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VBUS_UCBUS_HEAD_H_
+#define VBUS_UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusDrop : public VBus {
+  public:
+    void begin(void);
+    void begin(uint8_t _ownRxAddr);
+    void loop(void) override;
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr);
+    VBus_UCBusDrop(Vertex* _parent, String _name);
+};
+
+#endif 
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.cpp b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0e5cd5676e138fa0c17215c075181594ff48ac
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.cpp
@@ -0,0 +1,93 @@
+/*
+osap/vb_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusHead.h"
+#include "../osape/core/osap.h"
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusHead::VBus_UCBusHead(Vertex* _parent, String _name
+): VBus (_parent, _name) {
+  // report our address size,
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusHead::begin(void){
+  // start ucbus
+  ucBusHead_setup(); 
+}
+
+void VBus_UCBusHead::loop(void){
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      if (stackEmptySlot(this, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+void VBus_UCBusHead::timerISR(void){
+  ucBusHead_timerISR();
+}
+
+void VBus_UCBusHead::send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    OSAP::error("attempt to busf from head to self", MEDIUM);
+  } else {  
+    ucBusHead_transmitB(data, len, rxAddr);
+  }
+}
+
+boolean VBus_UCBusHead::cts(uint8_t rxAddr){
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void VBus_UCBusHead::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  ucBusHead_transmitA(data, len, broadcastChannel);
+}
+
+boolean VBus_UCBusHead::ctb(uint8_t broadcastChannel){
+  return ucBusHead_ctsA();
+}
+
+boolean VBus_UCBusHead::isOpen(uint8_t rxAddr){
+  return ucBusHead_isPresent(rxAddr);
+}
+
+#endif 
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.h b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfb7829f135f8ea04f193d3657f38cb15ea63cfa
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/osape_ucbus/vb_ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/vb_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusHead : public VBus {
+  public:
+    void begin(void);
+    // loop to ferry data, 
+    void loop(void) override;
+    // fast loop, needs to be called in ~ 10kHz ISR 
+    void timerISR(void);
+    // ... bus : osap API 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr) override;
+    // -------------------------------- Constructors 
+    VBus_UCBusHead(Vertex* _parent, String _name);
+};
+
+#endif
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/src/ucbus_config.h b/system/firmware/lpf-filament-sensor/src/ucbus_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..44de2305f344b1e1622dce3bde465959a8384232
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/src/ucbus_config.h
@@ -0,0 +1,31 @@
+/*
+ucbus_config.h
+
+config options for an ucbus instance 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_CONFIG_H_
+#define UCBUS_CONFIG_H_
+
+#define UCBUS_MAX_DROPS 32 
+
+#define UCBUS_ON_OSAP
+#define UCBUS_IS_DROP 
+//#define UCBUS_IS_HEAD 
+
+#define UCBUS_BAUD 2 
+
+//#define UCBUS_IS_D51
+#define UCBUS_IS_D21
+
+#define UCBUS_ON_OSAP 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-filament-sensor/test/README b/system/firmware/lpf-filament-sensor/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..b94d0890faa00a63737892509a5ca77ad3bdc6c3
--- /dev/null
+++ b/system/firmware/lpf-filament-sensor/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Unit Testing and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/page/plus/unit-testing.html
diff --git a/system/firmware/lpf-heater-module/.gitignore b/system/firmware/lpf-heater-module/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/system/firmware/lpf-heater-module/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/system/firmware/lpf-heater-module/.vscode/extensions.json b/system/firmware/lpf-heater-module/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/system/firmware/lpf-heater-module/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/system/firmware/lpf-heater-module/include/README b/system/firmware/lpf-heater-module/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/system/firmware/lpf-heater-module/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/system/firmware/lpf-heater-module/lib/README b/system/firmware/lpf-heater-module/lib/README
new file mode 100644
index 0000000000000000000000000000000000000000..6debab1e8b4c3faa0d06f4ff44bce343ce2cdcbf
--- /dev/null
+++ b/system/firmware/lpf-heater-module/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/system/firmware/lpf-heater-module/platformio.ini b/system/firmware/lpf-heater-module/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..50208f0cadb2e924ce3a661e45b5c88422dc71af
--- /dev/null
+++ b/system/firmware/lpf-heater-module/platformio.ini
@@ -0,0 +1,14 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:adafruit_feather_m4]
+platform = atmelsam
+board = adafruit_feather_m4
+framework = arduino
diff --git a/system/firmware/lpf-heater-module/src/drivers/peripheral_nums.h b/system/firmware/lpf-heater-module/src/drivers/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/drivers/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/indicators.h b/system/firmware/lpf-heater-module/src/indicators.h
new file mode 100644
index 0000000000000000000000000000000000000000..e80efb94bc21da90b167acc65850e5a43bd4b8e5
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/indicators.h
@@ -0,0 +1,19 @@
+// for the new one! with the DIP switch! 
+#define CLKLIGHT_PIN 27
+#define CLKLIGHT_PORT PORT->Group[0]
+#define ERRLIGHT_PIN 8
+#define ERRLIGHT_PORT PORT->Group[1]
+
+// PA27
+#define CLKLIGHT_BM (uint32_t)(1 << CLKLIGHT_PIN)
+#define CLKLIGHT_ON CLKLIGHT_PORT.OUTCLR.reg = CLKLIGHT_BM
+#define CLKLIGHT_OFF CLKLIGHT_PORT.OUTSET.reg = CLKLIGHT_BM
+#define CLKLIGHT_TOGGLE CLKLIGHT_PORT.OUTTGL.reg = CLKLIGHT_BM
+#define CLKLIGHT_SETUP CLKLIGHT_PORT.DIRSET.reg = CLKLIGHT_BM; CLKLIGHT_OFF
+
+// PB08 
+#define ERRLIGHT_BM (uint32_t)(1 << ERRLIGHT_PIN)
+#define ERRLIGHT_ON ERRLIGHT_PORT.OUTCLR.reg = ERRLIGHT_BM
+#define ERRLIGHT_OFF ERRLIGHT_PORT.OUTSET.reg = ERRLIGHT_BM
+#define ERRLIGHT_TOGGLE ERRLIGHT_PORT.OUTTGL.reg = ERRLIGHT_BM
+#define ERRLIGHT_SETUP ERRLIGHT_PORT.DIRSET.reg = ERRLIGHT_BM; ERRLIGHT_OFF
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/main.cpp b/system/firmware/lpf-heater-module/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..144af12a79705c8f9ee2261cb2c9ab39233086d9
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/main.cpp
@@ -0,0 +1,275 @@
+#include <Arduino.h>
+#include "indicators.h"
+#include "drivers/peripheral_nums.h"
+#include "thermalConfig.h"
+
+#include "osape/core/osap.h"
+#include "osape/vertices/endpoint.h"
+#include "osape_arduino/vp_arduinoSerial.h"
+#include "osape_ucbus/vb_ucBusDrop.h"
+
+// -------------------------------------------------------- DRIVERS
+
+// pa22 on TC4, peripheral e
+void startTC4(void){
+  PORT->Group[0].DIRSET.reg = (1 << 22);
+  PORT->Group[0].PINCFG[22].bit.PMUXEN = 1;
+  PORT->Group[0].PMUX[22 >> 1].reg = PORT_PMUX_PMUXE(PERIPHERAL_E);
+  // setup
+  TC4->COUNT16.CTRLA.bit.ENABLE = 0;
+  // clock
+  MCLK->APBCMASK.reg |= MCLK_APBCMASK_TC4;
+  // send clk, 
+  GCLK->PCHCTRL[TC4_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);
+  // setup
+  TC4->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV64
+    | TC_CTRLA_PRESCSYNC_GCLK;
+  TC4->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_NPWM;
+  TC4->COUNT16.CC[0].reg = 1; // timer counters in 16 bit mode have the top of the compare at the max width
+  // start 
+  while(TC4->COUNT16.SYNCBUSY.bit.ENABLE);
+  TC4->COUNT16.CTRLA.bit.ENABLE = 1;  
+  while(TC4->COUNT16.SYNCBUSY.bit.ENABLE);
+}
+
+void writeOutputA(float duty){
+  // duty 0-1, pwm counter 0-65535
+  if(duty < 0.0F){
+    duty = 0.0F;
+  } else if (duty > MAX_PWM){
+    duty = MAX_PWM;
+  }
+  uint16_t set = duty * 65535;
+  TC4->COUNT16.CCBUF[0].reg = set;
+}
+
+// PB11 on TC5-1
+void startTC5(void){
+  PORT->Group[1].DIRSET.reg = (1 << 11);
+  PORT->Group[1].PINCFG[11].bit.PMUXEN = 1;
+  PORT->Group[1].PMUX[11 >> 1].reg = PORT_PMUX_PMUXO(PERIPHERAL_E);
+  // setup
+  TC5->COUNT16.CTRLA.bit.ENABLE = 0;
+  // clock
+  MCLK->APBCMASK.reg |= MCLK_APBCMASK_TC5;
+  // send clk, 
+  GCLK->PCHCTRL[TC5_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);
+  // setup
+  TC5->COUNT16.CTRLA.reg = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV64
+    | TC_CTRLA_PRESCSYNC_GCLK;
+  TC5->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_NPWM;
+  TC5->COUNT16.CC[1].reg = 1; 
+  // start 
+  while(TC5->COUNT16.SYNCBUSY.bit.ENABLE);
+  TC5->COUNT16.CTRLA.bit.ENABLE = 1;  
+  while(TC5->COUNT16.SYNCBUSY.bit.ENABLE);
+}
+
+void writeOutputD(float duty){
+  if(duty < 0.0F){
+    duty = 0.0F;
+  } else if (duty > 1.0F){
+    duty = 1.0F;
+  }
+  uint16_t set = duty * 65535;
+  TC5->COUNT16.CCBUF[1].reg = set;
+}
+
+float readThermA(void){
+  // get 4, average 
+  uint32_t sum = 0;
+  for(uint8_t i = 0; i < 4; i ++){
+    sum += analogRead(A4);
+  }
+  float voltage = sum / 4 * (3.3 / 1024); // div sum, div vout 
+  // sysError(String(voltage));
+  // find the bounding temps, 
+  uint8_t lb = 0;
+  uint8_t ub = 0;
+  for(uint8_t i = 0; i < TABLE_SIZE - 1; i ++){
+    #if TABLE_DIR_UP
+    if(voltages[i] <= voltage && voltages[i + 1] > voltage){
+      lb = i + 1;
+      ub = i;
+      break;
+    }
+    #else
+    if(voltages[i] >= voltage && voltages[i + 1] < voltage){
+      lb = i;
+      ub = i + 1;
+      break;
+    }
+    #endif 
+  }
+  if(lb == 0 && ub == 0) return 0.0F; // no interval found, bail. 
+  // ok, have lb and ub, find position within 
+  float rel = (voltages[lb] - voltage) / (voltages[lb] - voltages[ub]);
+  // and interp. for temps, 
+  float temp = temps[lb] - (temps[lb] - temps[ub]) * rel;
+  return temp;
+}
+
+// -------------------------------------------------------- CONTROLLER VARS
+
+unsigned long lastRun = 0;
+unsigned long loopTime = 100; // ms,
+
+float _setPoint = 0.0F;         // desired temp, degs C
+float _tempReading = 0.0F;      // temp, degs C
+float _tempEstimate = 0.0F;     // filtered estimate 
+float _lastTempEstimate = 0.0F; // last estimate 
+float _alpha = 0.2F;            // filter alpha 
+float _currentOutput = 0.0F;    // 0-1, effort 
+float _PTerm = -1.5F;           // proportional 
+float _ITerm = 0.0F;            // integral (not used atm)
+float _DTerm = -2.6F;           // derivative
+
+// -------------------------------------------------------- OSAP 
+
+#ifdef IS_HOTEND
+OSAP osap("heater-module");
+#else
+OSAP osap("bed-heater-module");
+#endif 
+
+// -------------------------------------------------------- VPORTS 
+
+VPort_ArduinoSerial vpUSBSerial(&osap, "arduinoUSBSerial", &Serial);
+
+VBus_UCBusDrop vbUCBusDrop(&osap, "ucBusDrop");
+
+// -------------------------------------------------------- ENDPOINTS 
+
+// -------------------------------------------------------- Temp Set
+
+EP_ONDATA_RESPONSES onTempSetData(uint8_t* data, uint16_t len){
+  // set temp, float32, and write to endpoint 
+  chunk_float32 chunk = { .bytes = { data[0], data[1], data[2], data[3] } };
+  _setPoint = chunk.f;
+  //sysError("temp set: " + String(chunk.f));
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint tempSetEP(&osap, "tempSet", onTempSetData);
+
+// -------------------------------------------------------- Temp Query 
+
+Endpoint currentTempEP(&osap, "currentTemp");
+
+// -------------------------------------------------------- Effort Query 
+
+Endpoint currentOutputEP(&osap, "currentOutput");
+
+// -------------------------------------------------------- STATE UPDATES 
+
+void updateStates(void){
+  chunk_float32 chunkE = { .f = _currentOutput };
+  chunk_float32 chunkT = { .f = _tempEstimate };
+  // write 2 endpoints:
+  currentTempEP.write(chunkT.bytes, 4);
+  currentOutputEP.write(chunkE.bytes, 4);
+  // that's it ? 
+}
+
+// -------------------------------------------------------- PID Terms 
+
+EP_ONDATA_RESPONSES onPIDTermData(uint8_t* data, uint16_t len){
+  uint16_t rptr = 0;
+  chunk_float32 chunk[3];
+  chunk[0] = { .bytes = { data[rptr ++], data[rptr ++], data[rptr ++], data[rptr ++] } };
+  chunk[1] = { .bytes = { data[rptr ++], data[rptr ++], data[rptr ++], data[rptr ++] } };
+  chunk[2] = { .bytes = { data[rptr ++], data[rptr ++], data[rptr ++], data[rptr ++] } };
+  _PTerm = chunk[0].f;
+  _ITerm = chunk[1].f;
+  _DTerm = chunk[2].f;
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint pidTermsEP(&osap, "pidTerms", onPIDTermData);
+
+// -------------------------------------------------------- CONTROLLER
+
+// smoothing y [ n ] = α x [ n ] + ( 1 − α ) y [ n − 1 ]
+
+void runPID(void){
+  // 1st, read temp & write into endpoint, 
+  // then generate output & also write it both ways 
+  float sum = 0;
+  for(uint8_t i = 0; i < 4; i ++){
+    sum += readThermA();
+  }
+  _tempReading = sum / 4; // readThermA();
+  // filter
+  _tempEstimate = _tempReading * _alpha + _lastTempEstimate * (1 - _alpha);
+  // do error (positive error is too hot)
+  float err = _tempEstimate - _setPoint;
+  // derivative, with loop time of <loopTime> ms, 
+  float dt = (_tempEstimate - _lastTempEstimate) / ((float)loopTime / 1000);
+  _currentOutput = _PTerm * err + _DTerm * dt;
+  // write outputs to hardware
+  writeOutputA(_currentOutput);
+  // track,
+  _lastTempEstimate = _tempEstimate;
+}
+
+// -------------------------------------------------------- CHD Write 
+
+#ifdef IS_HOTEND
+
+EP_ONDATA_RESPONSES onChDData(uint8_t* data, uint16_t len){
+  chunk_float32 chunk = { .bytes = { data[0], data[1], data[2], data[3] } };
+  writeOutputD(chunk.f);
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint chDEP(&osap, "chD", onChDData);
+
+#endif 
+
+// -------------------------------------------------------- SETUP
+// from right to left, heater channels are ABCDEF
+// A: PA22, B: PA20, C: PB10 (TC5-0), D: PB11 (TC5-1), E: PB17, F: PA12 
+
+void setup() {
+  CLKLIGHT_SETUP;
+  ERRLIGHT_SETUP;
+  // setup comms 
+  vpUSBSerial.begin();
+  vbUCBusDrop.begin();
+  // start controller... 
+  startTC4();
+  // hotend, turn on a fan on PB10
+  #ifdef IS_HOTEND
+  // CHD / part cooling fan, 
+  //PORT->Group[1].DIRSET.reg = (1 << 11);
+  //PORT->Group[1].OUTSET.reg = (1 << 11);
+  startTC5();
+  TC5->COUNT16.CCBUF[1].reg = 6000;
+  // hotend cooling fan is always on, 
+  PORT->Group[1].DIRSET.reg = (1 << 10);
+  PORT->Group[1].OUTSET.reg = (1 << 10);
+  #endif
+}
+
+// -------------------------------------------------------- LOOP 
+
+void loop() {
+  // loop osap 
+  osap.loop();
+  // run loop, occasionally 
+  if(millis() > lastRun + loopTime){
+    lastRun = millis();
+    runPID();
+    updateStates();
+  }
+}
+
+// -------------------------------------------------------- HANDLES
+
+void ucBusDrop_onRxISR(void){
+
+}
+
+void ucBusDrop_onPacketARx(uint8_t* data, uint16_t len){
+  //ERRLIGHT_TOGGLE;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osap_config.h b/system/firmware/lpf-heater-module/src/osap_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..f94ddc11991022908e22357c21e15e17a03fd82f
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osap_config.h
@@ -0,0 +1,34 @@
+/*
+osap_config.h
+
+config options for an osap-embedded build 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_CONFIG_H_
+#define OSAP_CONFIG_H_
+
+// size of vertex stacks, lenght, then count,
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 3  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+#define VT_MAXITEMSPERTURN 8
+
+// max # of endpoints that could be spawned here,
+#define MAX_CONTEXT_ENDPOINTS 64
+
+// count of routes each endpoint can have, 
+#define ENDPOINT_MAX_ROUTES 4
+#define ENDPOINT_ROUTE_MAX_LEN 64 
+
+// count of broadcast channels width, 
+#define VBUS_MAX_BROADCAST_CHANNELS 64 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/LICENSE.md b/system/firmware/lpf-heater-module/src/osape/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/README.md b/system/firmware/lpf-heater-module/src/osape/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c94ebaff92a9980dbc93aa25047846ee4aa64e0
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/README.md
@@ -0,0 +1,5 @@
+## OSAP Embedded 
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/loop.cpp b/system/firmware/lpf-heater-module/src/osape/core/loop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c050974467d2fc95677d72f2e2da3b6608a0f588
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/loop.cpp
@@ -0,0 +1,255 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "loop.h"
+#include "packets.h"
+#include "osap.h"
+
+#define MAX_ITEMS_PER_LOOP 32
+//#define LOOP_DEBUG
+
+// we'll stack up to 64 messages to handle per loop, 
+// more items would cause issues: will throw errors and design circular looping at that point 
+stackItem* itemList[MAX_ITEMS_PER_LOOP];
+uint16_t itemListLen = 0;
+
+void listSetupRecursor(Vertex* vt){
+  // run the vertex' loop... but not if it's the root, yar 
+  if(vt->type != VT_TYPE_ROOT) vt->loop();
+  // for each input / output stack, try to collect all items... 
+  // alright I'm doing this collect... but want a kind of pickup-where-you-left-off thing, 
+  // so that we can have a fixed-length loop, i.e. 64 items per, but still do fairness... 
+  // otherwise our itemList has to be large enough to carry potentially every single item ? 
+  for(uint8_t od = 0; od < 2; od ++){
+    uint8_t count = stackGetItems(vt, od, &(itemList[itemListLen]), MAX_ITEMS_PER_LOOP - itemListLen);
+    itemListLen += count;
+  }
+  // recurse children...
+  for(uint8_t c = 0; c < vt->numChildren; c ++){
+    listSetupRecursor(vt->children[c]);
+  }
+}
+
+// sort-in-place based on time-to-death, 
+void listSort(stackItem** list, uint16_t listLen){
+  // write each item's time-to-death, 
+  uint32_t now = millis();
+  for(uint16_t i = 0; i < listLen; i ++){
+    list[i]->timeToDeath = ts_readUint16(list[i]->data, 0) - (now - list[i]->arrivalTime);
+  }
+  // also... vertex arrivalTime should be uint32_t milliseconds of arrival... 
+  #warning not-yet sorted... 
+}
+
+// this handles internal transport... checking for errors along paths, and running flowcontrol 
+// returns true to wipe current item, false to leave-in-wait, 
+boolean internalTransport(stackItem* item, uint16_t ptr){
+  // we walk thru our little internal tree here, 
+  Vertex* vt = item->vt;
+  // ptr for the walk, use item->data[ptr] == PK_INSTRUCTION, not PK_PTR, 
+  uint16_t fwdPtr = ptr + 1;
+  // count # of ops, 
+  uint8_t opCount = 0;
+  // for a max. of 16 fwd steps, 
+  for(uint8_t s = 0; s < 16; s ++){
+    uint16_t arg = readArg(item->data, fwdPtr);
+    switch(PK_READKEY(item->data[fwdPtr])){
+      // ---------------------------------------- Internal Dir Cases 
+      case PK_SIB:
+        // check validity of route & shift our reference vt,
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during sib transport"); return true;
+        } else if (arg >= vt->parent->numChildren){
+          OSAP::error("no sibling " + String(arg) + " at " + vt->name + " during sib transport"); return true;
+        } else {
+          // this is it: we go fwds to this vt & end-of-switch statements increment ptrs
+          vt = vt->parent->children[arg];
+        }
+        break;
+      case PK_PARENT:
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during parent transport"); return true;
+        } else {
+          // likewise... 
+          vt = vt->parent;
+        }
+        break;
+      case PK_CHILD:
+        if(arg >= vt->numChildren){
+          OSAP::error("no child " + String(arg) + " at " + vt->name + " during child transport"); return true;
+        } else {
+          // again, just walk fwds... 
+          vt = vt->children[arg];
+        }
+        break;
+      // ---------------------------------------- Terminal / Exit Cases 
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD: 
+      case PK_DEST:
+      case PK_PINGREQ:
+      case PK_SCOPEREQ:
+      case PK_LLESCAPE:
+        // check / transport...
+        if(stackEmptySlot(vt, VT_STACK_DESTINATION)){
+          // walk the ptr fwds, 
+          walkPtr(item->data, item->vt, opCount, ptr);
+          // ingest at the new place, 
+          stackLoadSlot(vt, VT_STACK_DESTINATION, item->data, item->len);
+          // return true to clear it out, 
+          return true;
+        } else {
+          return false; 
+        }
+      default:
+        OSAP::error("internal transport failure, ptr walk ends at unknown key");
+        return true;
+    } // end switch 
+    fwdPtr += 2;
+    opCount ++;
+  } // end max-16-steps, 
+  // if we're past all 16 and didn't hit a terminal, pckt is eggregiously long, rm it 
+  return true;
+}
+
+// -------------------------------------------------------- LOOP Begins Here 
+
+// ... would be breadth-first, ideally 
+void osapLoop(Vertex* root){
+  // we want to build a list of items, recursing through... 
+  itemListLen = 0;
+  listSetupRecursor(root);
+  // check now if items are nearly oversized...
+  // see notes in the log from 2022-06-22 if this error occurs, 
+  if(itemListLen >= MAX_ITEMS_PER_LOOP - 2){
+    OSAP::error("loop items exceeds " + String(MAX_ITEMS_PER_LOOP) + ", breaking per-loop transport properties... pls fix", HALTING);
+  }
+  // stash high-water mark,
+  if(itemListLen > OSAP::loopItemsHighWaterMark) OSAP::loopItemsHighWaterMark = itemListLen;
+  // log 'em 
+  // OSAP::debug("list has " + String(itemListLen) + " elements", LOOP);
+  // otherwise we can carry on... the item should be sorted, global vars, 
+  listSort(itemList, itemListLen);
+  // then we can handle 'em one by one 
+  for(uint16_t i = 0; i < itemListLen; i ++){
+    osapItemHandler(itemList[i]);
+  }
+}
+
+void osapItemHandler(stackItem* item){
+  // clear dead items, 
+  if(item->timeToDeath < 0){
+    OSAP::debug(  "item at " + item->vt->name + " times out w/ " + String(item->timeToDeath) + 
+                  " ms to live, of " + String(ts_readUint16(item->data, 0)) + " ttl", LOOP);
+    stackClearSlot(item);
+    return;
+  }
+  // get a ptr for the item, 
+  uint16_t ptr = 0;
+  if(!findPtr(item->data, &ptr)){    
+    OSAP::error("item at " + item->vt->name + " unable to find ptr, deleting...");
+    stackClearSlot(item);
+    return;
+  }
+  // now the handle-switch, item->data[ptr] = PK_PTR, we switch on instruction which is behind that, 
+  switch(PK_READKEY(item->data[ptr + 1])){
+    // ------------------------------------------ Terminal / Destination Switches 
+    case PK_DEST:
+      item->vt->destHandler(item, ptr);
+      break;
+    case PK_PINGREQ:
+      item->vt->pingRequestHandler(item, ptr);
+      break;
+    case PK_SCOPEREQ:
+      item->vt->scopeRequestHandler(item, ptr);
+      break;
+    case PK_PINGRES:
+    case PK_SCOPERES:
+      OSAP::error("ping or scope request issued to " + item->vt->name + " not handling those in embedded", MEDIUM);
+      stackClearSlot(item);
+      break;
+    // ------------------------------------------ Internal Transport 
+    case PK_SIB:
+    case PK_PARENT:
+    case PK_CHILD:  // transport handler returns true if msg should be wiped, false if it should be cycled
+      if(internalTransport(item, ptr)){
+        stackClearSlot(item);
+      }
+      break;
+    // ------------------------------------------ Network Transport 
+    case PK_PFWD:
+      // port forward...
+      if(item->vt->vport == nullptr){
+        OSAP::error("pfwd to non-vport " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        if(item->vt->vport->cts()){
+          // walk one step, but only if fn returns true (having success) 
+          if(walkPtr(item->data, item->vt, 1, ptr)) item->vt->vport->send(item->data, item->len);
+          stackClearSlot(item);
+        } else {
+          // failed to send this turn (flow controlled), will return here next round 
+        }
+      }
+      break;
+    case PK_BFWD:
+    case PK_BBRD:
+      // bus forward / bus broadcast: 
+      if(item->vt->vbus == nullptr){
+        OSAP::error("bfwd to non-vbus " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        // arg is rxAddr for bus-forwards, is broadcastChannel for bus-broadcast, 
+        uint16_t arg = readArg(item->data, ptr + 1);
+        if(item->data[ptr + 1] == PK_BFWD){
+          if(item->vt->vbus->cts(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              item->vt->vbus->send(item->data, item->len, arg);
+            } else {
+              OSAP::error("bfwd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bfwd (flow controlled), returning here next round... 
+          }
+        } else if (item->data[ptr + 1] == PK_BBRD){
+          if(item->vt->vbus->ctb(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              // OSAP::debug("broadcasting on ch " + String(arg));
+              item->vt->vbus->broadcast(item->data, item->len, arg);
+            } else {
+              OSAP::error("bbrd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bbrd, returning next... 
+          }
+        } else {
+          // doesn't make any sense, we switched in on these terms... 
+          OSAP::error("absolute nonsense", MEDIUM);
+          stackClearSlot(item);
+        }
+      }
+      break;
+    case PK_LLESCAPE:
+      OSAP::error("lldebug to embedded, dumping", MINOR);
+      stackClearSlot(item);
+      break;
+    default:
+      OSAP::error("unrecognized ptr to " + item->vt->name + " " + String(PK_READKEY(item->data[ptr + 1])), MINOR);
+      stackClearSlot(item);
+      // error, delete, 
+      break;
+  } // end swiiiitch 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/loop.h b/system/firmware/lpf-heater-module/src/osape/core/loop.h
new file mode 100644
index 0000000000000000000000000000000000000000..5022aa16c00da6b40864ca8f09432dab0744ad04
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/loop.h
@@ -0,0 +1,25 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef LOOP_H_
+#define LOOP_H_ 
+
+#include "vertex.h"
+
+// we loop, 
+void osapLoop(Vertex* root);
+// we handle, 
+void osapItemHandler(stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/osap.cpp b/system/firmware/lpf-heater-module/src/osape/core/osap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acde43271ecb27ea482e1b1079b02d847a15fed9
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/osap.cpp
@@ -0,0 +1,111 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "loop.h"
+#include "packets.h"
+#include "../utils/cobs.h"
+
+// stash most recents, and counts, and high water mark, 
+uint32_t OSAP::loopItemsHighWaterMark = 0;
+uint32_t errorCount = 0;
+uint32_t debugCount = 0;
+// strings...
+unsigned char latestError[VT_SLOTSIZE];
+unsigned char latestDebug[VT_SLOTSIZE];
+uint16_t latestErrorLen = 0;
+uint16_t latestDebugLen = 0;
+
+OSAP::OSAP(String _name) : Vertex("rt_" + _name){};
+
+void OSAP::loop(void){
+  // this is the root, so we kick all of the internal net operation from here 
+  osapLoop(this);
+}
+
+void OSAP::destHandler(stackItem* item, uint16_t ptr){
+  // classic switch on 'em 
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == ROOT_KEY, ptr + 3 = ID (if ack req.) 
+  uint16_t wptr = 0;
+  uint16_t len = 0;
+  switch(item->data[ptr + 2]){
+    case RT_DBG_STAT:
+    case RT_DBG_ERRMSG:
+    case RT_DBG_DBGMSG:
+      // return w/ the res key & same issuing ID 
+      payload[wptr ++] = PK_DEST;
+      payload[wptr ++] = RT_DBG_RES;
+      payload[wptr ++] = item->data[ptr + 3];
+      // stash high water mark, errormsg count, debugmsgcount 
+      ts_writeUint32(OSAP::loopItemsHighWaterMark, payload, &wptr);
+      ts_writeUint32(errorCount, payload, &wptr);
+      ts_writeUint32(debugCount, payload, &wptr);
+      // optionally, a string... I know we switch() then if(), it's uggo, 
+      if(item->data[ptr + 2] == RT_DBG_ERRMSG){
+        ts_writeString(latestError, latestErrorLen, payload, &wptr, VT_SLOTSIZE / 2);
+      } else if (item->data[ptr + 2] == RT_DBG_DBGMSG){
+        ts_writeString(latestDebug, latestDebugLen, payload, &wptr, VT_SLOTSIZE / 2);
+      }
+      // that's the payload, I figure, 
+      len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+      stackClearSlot(item);
+      stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      break;
+    default:
+      OSAP::error("unrecognized key to root node " + String(item->data[ptr + 2]));
+      stackClearSlot(item);
+      break;
+  }
+}
+
+uint8_t errBuf[255];
+uint8_t errBufEncoded[255];
+
+void debugPrint(String msg){
+  // whatever you want,
+  uint32_t len = msg.length();
+  // max this long, per the serlink bounds 
+  if(len + 9 > 255) len = 255 - 9;
+  // header... 
+  errBuf[0] = len + 8;  // len, key, cobs start + end, strlen (4) 
+  errBuf[1] = 172;      // serialLink debug key 
+  errBuf[2] = len & 255;
+  errBuf[3] = (len >> 8) & 255;
+  errBuf[4] = (len >> 16) & 255;
+  errBuf[5] = (len >> 24) & 255;
+  msg.getBytes(&(errBuf[6]), len + 1);
+  // encode from 2, leaving the len, key header... 
+  size_t ecl = cobsEncode(&(errBuf[2]), len + 4, errBufEncoded);
+  // what in god blazes ? copy back from encoded -> previous... 
+  memcpy(&(errBuf[2]), errBufEncoded, ecl);
+  // set tail to zero, to delineate, 
+  errBuf[errBuf[0] - 1] = 0;
+  // direct escape 
+  Serial.write(errBuf, errBuf[0]);
+}
+
+void OSAP::error(String msg, OSAPErrorLevels lvl){
+  //const char* str = msg.c_str();
+  msg.getBytes(latestError, VT_SLOTSIZE);
+  latestErrorLen = msg.length();
+  errorCount ++;
+  debugPrint(msg);
+}
+
+void OSAP::debug(String msg, OSAPDebugStreams stream){
+  msg.getBytes(latestDebug, VT_SLOTSIZE);
+  latestDebugLen = msg.length();
+  debugCount ++;
+  debugPrint(msg);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/osap.h b/system/firmware/lpf-heater-module/src/osape/core/osap.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c2c9d789ebd23ba452c7259c3423088ff2b9f
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/osap.h
@@ -0,0 +1,38 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+
+// largely semantic class, OSAP represents the root vertex in whichever context 
+// and it's where run the main loop from, etc... 
+// here is where we coordinate context-level stuff: adding new instances, 
+// stashing error messages & counts, etc, 
+
+enum OSAPErrorLevels { HALTING, MEDIUM, MINOR };
+enum OSAPDebugStreams { DEFAULT, LOOP };
+
+class OSAP : public Vertex {
+  public: 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr);
+    OSAP(String _name);// : Vertex(_name);
+    static void error(String msg, OSAPErrorLevels lvl = MINOR );
+    static void debug(String msg, OSAPDebugStreams stream = DEFAULT );
+    static uint32_t loopItemsHighWaterMark;
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/packets.cpp b/system/firmware/lpf-heater-module/src/osape/core/packets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf83928d99d3c173d0efdef40ab614dc2433b409
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/packets.cpp
@@ -0,0 +1,193 @@
+/*
+osap/packets.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "packets.h"
+#include "ts.h"
+#include "osap.h"
+
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg){
+  buf[ptr] = key | (0b00001111 & (arg >> 8));
+  buf[ptr + 1] = arg & 0b11111111;
+}
+// not sure how I want to do this yet... 
+uint16_t readArg(uint8_t* buf, uint16_t ptr){
+  return ((buf[ptr] & 0b00001111) << 8) | buf[ptr + 1];
+}
+
+boolean findPtr(uint8_t* pck, uint16_t* pt){
+  // 1st instruction is always at pck[4], pck[0][1] == ttl, pck[2][3] == segSize 
+  uint16_t ptr = 4;
+  // there's a potential speedup where we assume given *pt is already incremented somewhat, 
+  // maybe shaves some ns... but here we just look fresh every time, 
+  for(uint8_t i = 0; i < 16; i ++){
+    switch(PK_READKEY(pck[ptr])){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        ptr += 2;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+boolean walkPtr(uint8_t* pck, Vertex* source, uint8_t steps, uint16_t ptr){
+  // if the ptr we were handed isn't in the right spot, try to find it... 
+  if(pck[ptr] != PK_PTR){
+    // if that fails, bail... 
+    if(!findPtr(pck, &ptr)){
+      OSAP::error("before a ptr walk, ptr is out of place...");
+      return false;
+    }
+  }
+  // carry on w/ the walking algo, 
+  for(uint8_t s = 0; s < steps; s ++){
+    switch PK_READKEY(pck[ptr + 1]){
+      case PK_SIB:
+        {
+          // stash indice from-whence it came,
+          uint16_t txIndice = source->indice;
+          // for loop's next step, this is the source now, 
+          source = source->parent->children[readArg(pck, ptr + 1)];
+          // where ptr is currently, we stash new key/pair for a reversal, 
+          writeKeyArgPair(pck, ptr, PK_SIB, txIndice);
+          // increment packet's ptr, and our own... 
+          pck[ptr + 2] = PK_PTR; 
+          ptr += 2;
+        }
+        break;
+      case PK_PARENT:
+        // reversal for a 'parent' instruction is to bounce back down to the child, 
+        writeKeyArgPair(pck, ptr, PK_CHILD, source->indice);
+        // next source is now...
+        source = source->parent;
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      case PK_CHILD:
+        // next source is... 
+        source = source->children[readArg(pck, ptr + 1)];
+        // reversal for 'child' instruction is to go back up to parent, 
+        writeKeyArgPair(pck, ptr, PK_PARENT, 0);
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2; 
+        break;
+      case PK_PFWD:
+        // reversal for pfwd instruction is identical, 
+        writeKeyArgPair(pck, ptr, PK_PFWD, 0);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // though this should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have port fwd w/ more than one step");
+          return false;
+        }
+        break;
+      case PK_BFWD:
+        // reversal for bfwd instruction is to return *up*... 
+        writeKeyArgPair(pck, ptr, PK_BFWD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // this also should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have bus fwd w/ more than one step");
+          return false; 
+        }
+        break;
+      case PK_BBRD:
+        // broadcasts are a little strange, we also stuff the ownRxAddr in,
+        writeKeyArgPair(pck, ptr, PK_BBRD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      default:
+        OSAP::error("have out of place keys in the ptr walk...");
+        return false;
+    }
+  } // end steps, alleged success,  
+  return true; 
+}
+
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen){
+  uint16_t wptr = 0;
+  ts_writeUint16(route->ttl, gram, &wptr);
+  ts_writeUint16(route->segSize, gram, &wptr);
+  memcpy(&(gram[wptr]), route->path, route->pathLen);
+  wptr += route->pathLen;
+  if(wptr + payloadLen > route->segSize){
+    OSAP::error("writeDatagram asked to write packet that exceeds segSize, bailing", MEDIUM);
+    return 0;
+  }
+  memcpy(&(gram[wptr]), payload, payloadLen);
+  wptr += payloadLen;
+  return wptr;
+}
+
+// original gram, payload, len, 
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen){
+  // 1st up, we can straight copy the 1st 4 bytes, 
+  memcpy(gram, ogGram, 4);
+  // now find a ptr, 
+  uint16_t ptr = 0;
+  if(!findPtr(ogGram, &ptr)){
+    OSAP::error("writeReply can't find the pointer...", MEDIUM);
+    return 0;
+  }
+  // do we have enough space? it's the minimum of the allowed segsize & stated maxGramLength, 
+  maxGramLength = min(maxGramLength, ts_readUint16(ogGram, 2));
+  if(ptr + 1 + payloadLen > maxGramLength){
+    OSAP::error("writeReply asked to write packet that exceeds maxGramLength, bailing", MEDIUM);
+    return 0;
+  }
+  // write the payload in, apres-pointer, 
+  memcpy(&(gram[ptr + 1]), payload, payloadLen);
+  // now we can do a little reversing... 
+  uint16_t wptr = 4;
+  uint16_t end = ptr;
+  uint16_t rptr = ptr;
+  // 1st byte... the ptr, 
+  gram[wptr ++] = PK_PTR;
+  // now for a max 16 steps, 
+  for(uint8_t h = 0; h < 16; h ++){
+    if(wptr >= end) break;
+    rptr -= 2;
+    switch(PK_READKEY(ogGram[rptr])){
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        gram[wptr ++] = ogGram[rptr];
+        gram[wptr ++] = ogGram[rptr + 1];
+        break;
+      default:
+        OSAP::error("writeReply fails to reverse this packet, bailing", MEDIUM);
+        return 0;
+    }
+  } // end thru-loop, 
+  // it's written, return the len  // we had gram[ptr] = PK_PTR, so len was ptr + 1, then added payloadLen, 
+  return end + 1 + payloadLen;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/packets.h b/system/firmware/lpf-heater-module/src/osape/core/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..914656be1eb7656f481915438a10701edad23280
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/packets.h
@@ -0,0 +1,48 @@
+/*
+osap/packets.h
+
+reading / writing from osap packets / datagrams 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_PACKETS_H_
+#define OSAP_PACKETS_H_
+
+#include <Arduino.h>
+#include "vertex.h"
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 240
+#define PK_DEST 224
+#define PK_PINGREQ 192 
+#define PK_PINGRES 176 
+#define PK_SCOPEREQ 160 
+#define PK_SCOPERES 144 
+#define PK_SIB 16 
+#define PK_PARENT 32 
+#define PK_CHILD 48 
+#define PK_PFWD 64 
+#define PK_BFWD 80
+#define PK_BBRD 96 
+#define PK_LLESCAPE 112 
+
+// to read *just the key* from key, arg pair
+#define PK_READKEY(data) (data & 0b11110000)
+
+// packet utes, 
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg);
+uint16_t readArg(uint8_t* buf, uint16_t ptr);
+boolean findPtr(uint8_t* pck, uint16_t* ptr);
+boolean walkPtr(uint8_t* pck, Vertex* vt, uint8_t steps, uint16_t ptr = 4);
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen);
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/routes.cpp b/system/firmware/lpf-heater-module/src/osape/core/routes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6caea0c0a00c56f339f2fdb7ec4b02278e1faf73
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/routes.cpp
@@ -0,0 +1,55 @@
+/*
+osap/routes.cpp
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "routes.h"
+#include "packets.h"
+
+Route::Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize){
+  ttl = _ttl;
+  segSize = _segSize;
+  // nope, 
+  if(_pathLen > 64){
+    _pathLen = 0;
+  }
+  memcpy(path, _path, _pathLen);
+  pathLen = _pathLen;
+}
+
+Route::Route(void){
+  path[pathLen ++] = PK_PTR;
+}
+
+Route* Route::sib(uint16_t indice){
+  writeKeyArgPair(path, pathLen, PK_SIB, indice);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::pfwd(void){
+  writeKeyArgPair(path, pathLen, PK_PFWD, 0);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bfwd(uint16_t rxAddr){
+  writeKeyArgPair(path, pathLen, PK_BFWD, rxAddr);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bbrd(uint16_t channel){
+  writeKeyArgPair(path, pathLen, PK_BBRD, channel);
+  pathLen += 2;
+  return this; 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/routes.h b/system/firmware/lpf-heater-module/src/osape/core/routes.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2bb3c97cffb7df24867de4efe7489b40daa4a0e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/routes.h
@@ -0,0 +1,38 @@
+/*
+osap/routes.h
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_ROUTES_H_
+#define OSAP_ROUTES_H_
+
+#include <Arduino.h>
+
+// a route type... 
+class Route {
+  public:
+    uint8_t path[64];
+    uint16_t pathLen = 0;
+    uint16_t ttl = 1000;
+    uint16_t segSize = 128;
+    // write-direct constructor, 
+    Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize);
+    // write-along constructor, 
+    Route(void);
+    // pass-thru initialize constructors, 
+    Route* sib(uint16_t indice);
+    Route* pfwd(void);
+    Route* bfwd(uint16_t rxAddr);
+    Route* bbrd(uint16_t channel);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/stack.cpp b/system/firmware/lpf-heater-module/src/osape/core/stack.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..401bd7103f872bd141172d945ff2b2a8cb93e36f
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/stack.cpp
@@ -0,0 +1,138 @@
+/*
+osap/stack.cpp
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "stack.h"
+#include "vertex.h"
+#include "osap.h"
+
+// ---------------------------------------------- Stack Tools 
+
+void stackReset(Vertex* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+      // and ptrs to self, 
+      vt->stack[od][s].vt = vt;
+      vt->stack[od][s].od = od;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(Vertex* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //DEBUG("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item){
+  // this would be deadly, so:
+  if(od > 1) {
+    OSAP::error("stackClearSlot, od > 1, badness", MEDIUM);
+    return;
+  }
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+  }
+  // now we callback to the vertex; these fns are often used to clear flowcontrol condns 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      vt->onDestinationStackClear(indice);
+      break;
+    default:  // guarded against this above... 
+      break;
+  }
+}
+
+void stackClearSlot(stackItem* item){
+  stackClearSlot(item->vt, item->od, item);
+}
diff --git a/system/firmware/lpf-heater-module/src/osape/core/stack.h b/system/firmware/lpf-heater-module/src/osape/core/stack.h
new file mode 100644
index 0000000000000000000000000000000000000000..79151239b987f025150dc6f1ac580cfc4e474887
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/stack.h
@@ -0,0 +1,54 @@
+/*
+osap/stack.h
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STACK_H_
+#define STACK_H_ 
+
+#include <Arduino.h>
+#include "./osap_config.h" 
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+class Vertex;
+
+// core routing layer chunk-of-stuff, 
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+typedef struct stackItem {
+  uint8_t data[VT_SLOTSIZE];          // data bytes
+  uint16_t len = 0;                   // data bytes count 
+  uint32_t arrivalTime = 0;           // ms-since-system-alive, time at last ingest
+  int32_t timeToDeath = 0;            // ms of time until pckt vanishes on this hop
+  Vertex* vt;                         // vertex to whomst we belong, 
+  uint8_t od;                         // origin / destination to which we belong, 
+  uint8_t indice;                     // actual physical position in the stack 
+  uint16_t ptr = 0;                   // current data[ptr] == 88 
+  stackItem* next = nullptr;          // linked ringbuffer next 
+  stackItem* previous = nullptr;      // linked ringbuffer previous 
+} stackItem;
+
+// stack setup / reset 
+void stackReset(Vertex* vt);
+
+// stack origin side 
+boolean stackEmptySlot(Vertex* vt, uint8_t od);
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// stack exit side 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item);
+void stackClearSlot(stackItem* item);
+
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/ts.cpp b/system/firmware/lpf-heater-module/src/osape/core/ts.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cd3fdc9c1c249d25b22b75fa9fc69f311d04c19
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/ts.cpp
@@ -0,0 +1,183 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+// ---------------------------------------------- Reading 
+
+// boolean 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr){
+  if(buf[(*ptr) ++]){
+    *val = true;
+  } else {
+    *val = false;
+  }
+}
+
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr){
+  boolean val = buf[(*ptr)] ? true : false;
+  (*ptr) += 1;
+  return val;
+}
+
+// uint8 
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr){
+  uint8_t val = buf[(*ptr)];
+  (*ptr) += 1;
+  return val;
+}
+
+// uint16 
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+#warning some of these are pretty vague, i.e. this ingests a pointer *not as a pointer* (lol)
+// so it doesn't increment it, whereas the readUint8 above *does so* - ... ?? pick a style ? 
+uint16_t ts_readUint16(unsigned char* buf, uint16_t ptr){
+  return (buf[ptr + 1] << 8) | buf[ptr];
+}
+
+// uint32 
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr){
+  uint32_t val = (buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)]);
+  (*ptr) += 4;
+  return val;
+}
+
+// int32 
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.i;
+}
+
+// float32 
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.f;
+}
+
+// -------------------------------------------------------- Writing 
+
+// boolean
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+// unsigned 
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+// signed 
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int16 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+}
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+// floats 
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+// string, overloaded ?
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen){
+  if(strLen > maxLen) strLen = maxLen;
+  buf[(*ptr) ++] = strLen & 255;
+  buf[(*ptr) ++] = (strLen >> 8) & 255;
+  buf[(*ptr) ++] = (strLen >> 16) & 255;
+  buf[(*ptr) ++] = (strLen >> 24) & 255;
+  // write in one-by-one, surely there is a better way, 
+  for(uint16_t i = 0; i < strLen; i ++){
+    buf[(*ptr) ++] = str[i];
+  }
+  *ptr += strLen;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/system/firmware/lpf-heater-module/src/osape/core/ts.h b/system/firmware/lpf-heater-module/src/osape/core/ts.h
new file mode 100644
index 0000000000000000000000000000000000000000..63e77b2b02c0f7716bb1cba55cc9eef613d1207f
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/ts.h
@@ -0,0 +1,157 @@
+/*
+core/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22       // top level 
+#define VT_TYPE_MODULE 23     // collection of things (?) or something, idk yet 
+#define VT_TYPE_ENDPOINT 24   // software endpoint w/ read/write semantics 
+#define VT_TYPE_QUERY 25 
+#define VT_TYPE_ENDPOINT_MULTISEG 26 // likewise, but requring multisegment transmission 
+#define VT_TYPE_CODE 25       // autonomous graph dwellers 
+#define VT_TYPE_VPORT 44      // virtual ports 
+#define VT_TYPE_VBUS 45       // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+#define EP_ROUTE_QUERY_REQ 141 
+#define EP_ROUTE_QUERY_RES 142
+#define EP_ROUTE_SET_REQ 143
+#define EP_ROUTE_SET_RES 144 
+#define EP_ROUTE_RM_REQ 147
+#define EP_ROUTE_RM_RES 148 
+
+#define EP_ROUTEMODE_ACKED 167
+#define EP_ROUTEMODE_ACKLESS 168 
+
+// -------------------------------------------------------- Root Keys 
+
+#define RT_DBG_STAT 151
+#define RT_DBG_ERRMSG 152 
+#define RT_DBG_DBGMSG 153
+#define RT_DBG_RES 161
+
+// -------------------------------------------------------- VBus MVC Keys 
+
+#define VBUS_BROADCAST_MAP_REQ 145
+#define VBUS_BROADCAST_MAP_RES 146
+#define VBUS_BROADCAST_QUERY_REQ 141
+#define VBUS_BROADCAST_QUERY_RES 142
+#define VBUS_BROADCAST_SET_REQ 143
+#define VBUS_BROADCAST_SET_RES 144 
+#define VBUS_BROADCAST_RM_REQ 147 
+#define VBUS_BROADCAST_RM_RES 148 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int16 {
+  uint8_t bytes[2];
+  int16_t i;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr);
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr);
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+uint16_t ts_readUint16(uint8_t* buf, uint16_t ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr);
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+// -------------------------------------------------------- Writing 
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/vertex.cpp b/system/firmware/lpf-heater-module/src/osape/core/vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9ce012af681a42b059c6585888d1db806dd2ab51
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/vertex.cpp
@@ -0,0 +1,327 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "stack.h"
+#include "osap.h"
+#include "packets.h"
+
+// ---------------------------------------------- Temporary Stash 
+
+uint8_t Vertex::payload[VT_SLOTSIZE];
+uint8_t Vertex::datagram[VT_SLOTSIZE];
+
+// ---------------------------------------------- Vertex Constructor and Defaults 
+
+Vertex::Vertex( 
+  Vertex* _parent, String _name, 
+  void (*_loop)(Vertex* vt),
+  void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+  void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+){
+  // name self, reset stack... 
+  name = _name;
+  stackReset(this);
+  // callback assignments... 
+  loop_cb = _loop;
+  onOriginStackClear_cb = _onOriginStackClear;
+  onDestinationStackClear_cb = _onDestinationStackClear;
+  // insert self to osap net,
+  if(_parent == nullptr){
+    type = VT_TYPE_ROOT;
+    indice = 0;
+  } else {
+    if (_parent->numChildren >= VT_MAXCHILDREN) {
+      OSAP::error("trying to nest a vertex under " + _parent->name + " but we have reached VT_MAXCHILDREN limit", HALTING);
+    } else {
+      this->indice = _parent->numChildren;
+      this->parent = _parent;
+      _parent->children[_parent->numChildren ++] = this;
+    }
+  }
+}
+
+void Vertex::loop(void){
+  if(loop_cb != nullptr) return loop_cb(this);
+}
+
+void Vertex::destHandler(stackItem* item, uint16_t ptr){
+  // generic handler...
+  OSAP::debug("generic destHandler at " + name);
+  stackClearSlot(item);
+}
+
+void Vertex::pingRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_PINGRES;
+  payload[1] = item->data[ptr + 2];
+  // write a new gram, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 2);
+  // clear previous, 
+  stackClearSlot(item);
+  // load next... there will be one empty, as this has just arrived here... & we just wiped it 
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+void Vertex::scopeRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_SCOPERES;
+  payload[1] = item->data[ptr + 2];
+  // next items write starting here, 
+  uint16_t wptr = 2;
+  // scope time-tag, 
+  ts_writeUint32(scopeTimeTag, payload, &wptr);
+  // and read in the previous scope (this is traversal state required to delineate loops in the graph) 
+  uint16_t rptr = ptr + 3;
+  ts_readUint32(&scopeTimeTag, item->data, &rptr);
+  // write the vertex type,  
+  payload[wptr ++] = type;
+  // vport / vbus link states, 
+  if(type == VT_TYPE_VPORT){
+    payload[wptr ++] = (vport->isOpen() ? 1 : 0);
+  } else if (type == VT_TYPE_VBUS){
+    uint16_t addrSize = vbus->addrSpaceSize;
+    uint16_t addr = 0;
+    // ok we write the address size in first, then our own rxaddr, 
+    ts_writeUint16(vbus->addrSpaceSize, payload, &wptr);
+    ts_writeUint16(vbus->ownRxAddr, payload, &wptr);
+    // then *so long a we're not overwriting*, we stuff link-state bytes, 
+    while(wptr + 8 + name.length() <= VT_SLOTSIZE){
+      payload[wptr] = 0;
+      for(uint8_t b = 0; b < 8; b ++){
+        payload[wptr] |= (vbus->isOpen(addr) ? 1 : 0) << b;
+        addr ++;
+        if(addr >= addrSize) goto end;
+      }
+      wptr ++;
+    }
+    end:
+    wptr ++; // += 1 more, so we write into next, 
+  }
+  // our own indice, # siblings, and # children, 
+  ts_writeUint16(indice, payload, &wptr);
+  if(parent != nullptr){
+    ts_writeUint16(parent->numChildren, payload, &wptr);
+  } else {
+    ts_writeUint16(0, payload, &wptr);
+  }
+  ts_writeUint16(numChildren, payload, &wptr);
+  // finally, our string name:
+  ts_writeString(name, payload, &wptr);
+  // and roll that back up, rm old, and ship it, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+  stackClearSlot(item);
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+
+void Vertex::onOriginStackClear(uint8_t slot){
+  if(onOriginStackClear_cb != nullptr) return onOriginStackClear_cb(this, slot);
+}
+
+void Vertex::onDestinationStackClear(uint8_t slot){
+  if(onDestinationStackClear_cb != nullptr) return onDestinationStackClear_cb(this, slot);
+}
+
+// ---------------------------------------------- VPort Constructor and Defaults 
+
+VPort::VPort(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vp_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VPORT;
+  vport = this; 
+}
+
+// ---------------------------------------------- VBus Constructor and Defaults 
+
+VBus::VBus(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vb_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VBUS;
+  vbus = this;
+  // these should all init to nullptr, 
+  for(uint8_t ch = 0; ch < VBUS_MAX_BROADCAST_CHANNELS; ch ++){
+    broadcastChannels[ch] = nullptr;
+  }
+}
+
+void VBus::injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  // ok so first we want to see if we have anything sub'd to this channel, so
+  if(broadcastChannels[broadcastChannel] != nullptr){
+    // we have a route, so we want to load this data *as we inject some new path segments* 
+    Route* route = broadcastChannels[broadcastChannel];
+    // we could definitely do this faster w/o using the stackLoadSlot fn, but we won't do that yet... 
+    // will use the vertex-global datagram stash for that 
+    uint16_t ptr = 0; 
+    if(!findPtr(data, &ptr)){ OSAP::error("can't find ptr during broadcast injest", MEDIUM); return; }
+    // packet should look like 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <payload>
+    // we want to inject the channel's route such that 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <ch_route>, <payload>
+    // shouldn't actually be too difficult, eh?
+    // we do need to guard on lengths, 
+    if(len + route->pathLen > VT_SLOTSIZE){ OSAP::error("datagram + channel route is too large", MEDIUM); return; }
+    // copy up to PTR: pck[ptr] == PK_PTR, so we want to *include* this byte, having len ptr + 1, 
+    memcpy(datagram, data, ptr + 1);
+    // copy in route, but recall that as initialized, route->path[0] == PK_PTR, we don't want to double that up, 
+    memcpy(&(datagram[ptr + 1]), &(route->path[1]), route->pathLen - 1);
+    // then the rest of the gram, from just after-the-ptr, to end, 
+    memcpy(&datagram[ptr + 1 + route->pathLen - 1], &(data[ptr + 1]), len - ptr - 1);
+    // now we can load this in, 
+    stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len + route->pathLen - 1);
+    // aye that's it innit? 
+  }
+}
+
+void VBus::setBroadcastChannel(uint8_t channel, Route* route){
+  if(channel >= VBUS_MAX_BROADCAST_CHANNELS) return;
+  // seems a little sus, idk 
+  broadcastChannels[channel] = route;
+}
+
+void VBus::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == the key we're switching on...
+  switch(item->data[ptr + 2]){
+    case VBUS_BROADCAST_MAP_REQ:
+      // mvc request a map of our active broadcast channels, this is akin to bus link-state-scope packet
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_MAP_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // max length of channels... max 255, same as max endpoint routes (?) 
+        // this is maybe an error, consult packet spec (transport layer) for completeness, 
+        // time being... rare to have > 255 broadcast channels, 
+        payload[wptr ++] = VBUS_MAX_BROADCAST_CHANNELS;
+        // then *so long a we're not overwriting*, we stuff link-state bytes, 
+        // idk, 32 is arbitrary, we have to account for return-route length properly... 
+        uint16_t channel = 0;
+        while(wptr + 32 <= VT_SLOTSIZE){
+          payload[wptr] = 0;
+          for(uint8_t b = 0; b < 8; b ++){
+            payload[wptr] |= (broadcastChannels[channel] == nullptr ? 0 : 1) << b;
+            channel ++;
+            if(channel >= VBUS_MAX_BROADCAST_CHANNELS) goto end;
+          }
+          wptr ++;
+        }
+        end:
+        wptr ++; // += 1 more, so we write into next, 
+        // we're ready to write the reply back, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_QUERY_REQ:
+      // mvc requests broadcast channel info on a particular channel, 
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_QUERY_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // the indice of the channel we're looking at, 
+        uint16_t ch = item->data[ptr + 4];
+        // if the ch exists, 
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS && broadcastChannels[ch] != nullptr){
+          payload[wptr ++] = 1;
+          // now... these are route objects, but we only use the path part... 
+          // but we'll re-use route-object serialization schemes from EP_ROUTE_QUERY_REQ 
+          ts_writeUint16(broadcastChannels[ch]->ttl, payload, &wptr);
+          ts_writeUint16(broadcastChannels[ch]->segSize, payload, &wptr);
+          // path copy 
+          memcpy(&(payload[wptr]), broadcastChannels[ch]->path, broadcastChannels[ch]->pathLen);
+          wptr += broadcastChannels[ch]->pathLen;
+        } else {
+          payload[wptr ++] = 0;
+        }
+        // write reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_SET_REQ:
+      // mvc requests to set a broadcast channel route 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // ch to write into...
+        uint8_t ch = item->data[ptr + 4];
+        // reply-write-pointer 
+        uint16_t wptr = 0;
+        // prep a response, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_SET_RES;
+        payload[wptr ++] = id;
+        if(ch >= VBUS_MAX_BROADCAST_CHANNELS){
+          // won't go 
+          OSAP::error("attempt to write to oob broadcast channel");
+          payload[wptr ++] = 0;
+        } else {
+          // should go 
+          payload[wptr ++] = 1;          
+          if(broadcastChannels[ch] != nullptr) OSAP::debug("overwriting previous broadcast ch at " + String(ch));
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          setBroadcastChannel(ch, new Route(path, pathLen, ttl, segSize));
+        }
+        // in any case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_RM_REQ:
+      // mvc requests to rm a broadcast channel, 
+      // todo / cleanliness: might be salient to 'write 0' to delete (?) who knows 
+      {
+        // id & indice to rm 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t ch = item->data[ptr + 4];
+        uint16_t wptr = 0;
+        // prep res 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_RM_RES;
+        payload[wptr ++] = id;
+        // can we rm ?
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS){
+          if(broadcastChannels[ch] != nullptr) {
+            delete broadcastChannels[ch];
+            broadcastChannels[ch] = nullptr;
+            payload[wptr ++] = 1;
+          } else {
+            // didn't exist, so, a bad delete: 
+            payload[wptr ++] = 0;
+          }
+        } else {
+          // bad req, should throw errors... 
+          payload[wptr ++] = 0;
+        }
+        // can send now, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    default:
+      OSAP::error("vbus rx msg w/ unrecognized vbus key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/core/vertex.h b/system/firmware/lpf-heater-module/src/osape/core/vertex.h
new file mode 100644
index 0000000000000000000000000000000000000000..842d5733f64fa6661165c84e2193b2c0604892d1
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/core/vertex.h
@@ -0,0 +1,131 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "routes.h"
+#include "stack.h"
+// vertex config is build dependent, define in <folder-containing-osape>/osapConfig.h 
+#include "./osap_config.h" 
+
+// we have the vertex type, 
+// since it contains ptrs to others of its type, we fwd declare the type...
+class Vertex;
+// ... 
+typedef struct stackItem stackItem;
+typedef struct VPort VPort;
+typedef struct VBus VBus;
+
+// default vt fns 
+void vtLoopDefault(Vertex* vt);
+void vtOnOriginStackClearDefault(Vertex* vt, uint8_t slot);
+void vtOnDestinationStackClearDefault(Vertex* vt, uint8_t slot);
+
+// addressable node in the graph ! 
+class Vertex {
+  public:
+    // just temporary stashes, used all over the place to prep messages... 
+    static uint8_t payload[VT_SLOTSIZE];
+    static uint8_t datagram[VT_SLOTSIZE];
+    // -------------------------------- FN PTRS 
+    // these are *genuine function ptrs* not member functions, my dudes 
+    void (*loop_cb)(Vertex* vt) = nullptr;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    void (*onDestinationStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    // -------------------------------- Methods
+    virtual void loop(void);
+    virtual void destHandler(stackItem* item, uint16_t ptr);
+    void pingRequestHandler(stackItem* item, uint16_t ptr);
+    void scopeRequestHandler(stackItem* item, uint16_t ptr);
+    virtual void onOriginStackClear(uint8_t slot);
+    virtual void onDestinationStackClear(uint8_t slot);
+    // -------------------------------- DATA
+    // a type, a position, a name 
+    uint8_t type = VT_TYPE_CODE;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we were last scoped (need for graph traversals, final implementation tbd)
+    uint32_t scopeTimeTag = 0;
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    Vertex* parent = nullptr;
+    Vertex* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // sometimes a vertex is a vport, sometimes it is a vbus, 
+    VPort* vport;
+    VBus* vbus;
+    // -------------------------------- CONSTRUCTORS 
+    Vertex( 
+      Vertex* _parent, 
+      String _name, 
+      void (*_loop)(Vertex* vt),
+      void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+      void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+    );
+    Vertex(Vertex* _parent, String _name) : Vertex(_parent, _name, nullptr, nullptr, nullptr){};
+    Vertex(String _name) : Vertex(nullptr, _name, nullptr, nullptr, nullptr){};
+};
+
+// ---------------------------------------------- VPort 
+
+class VPort : public Vertex {
+  public:
+    // -------------------------------- OK these bbs are methods, 
+    virtual void send(uint8_t* data, uint16_t len) = 0;
+    virtual boolean cts(void) = 0;
+    virtual boolean isOpen(void) = 0;
+    // base constructor, 
+    VPort(Vertex* _parent, String _name);
+};
+
+// ---------------------------------------------- VBus 
+
+class VBus : public Vertex{
+  public:
+    // -------------------------------- Methods: these are purely virtual... 
+    virtual void send(uint8_t* data, uint16_t len, uint8_t rxAddr) = 0;
+    virtual void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) = 0;
+    // clear to send, clear to broadcast, 
+    virtual boolean cts(uint8_t rxAddr) = 0;
+    virtual boolean ctb(uint8_t broadcastChannel) = 0;
+    // link state per rx-addr,
+    virtual boolean isOpen(uint8_t rxAddr) = 0;
+    // handle things aimed at us, for mvc etc 
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // busses can read-in to broadcasts,
+    void injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel);
+    // we have also... broadcast channels... these are little route stubs & channel pairs, which we just straight up index, 
+    Route* broadcastChannels[VBUS_MAX_BROADCAST_CHANNELS];
+    // have to update those... 
+    void setBroadcastChannel(uint8_t channel, Route* route);
+    // has an rx addr, 
+    uint16_t ownRxAddr = 0;
+    // has a width-of-addr-space, 
+    uint16_t addrSpaceSize = 0;
+    // base constructor, children inherit... 
+    VBus(Vertex* _parent, String _name);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/utils/cobs.cpp b/system/firmware/lpf-heater-module/src/osape/utils/cobs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81cc05bb3b38d85273a838a4b05df31bff2783a9
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/system/firmware/lpf-heater-module/src/osape/utils/cobs.h b/system/firmware/lpf-heater-module/src/osape/utils/cobs.h
new file mode 100644
index 0000000000000000000000000000000000000000..b47070ca26d021f113da680a6835df65712d4007
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.cpp b/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d9fe310be794e69ef9040e2ee33a26bcf986f9
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.cpp
@@ -0,0 +1,351 @@
+/*
+osape/vertices/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "../core/osap.h"
+#include "../core/packets.h"
+
+// -------------------------------------------------------- Constructors 
+
+// route constructor 
+EndpointRoute::EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+  if(_mode != EP_ROUTEMODE_ACKED && _mode != EP_ROUTEMODE_ACKLESS){
+    _mode = EP_ROUTEMODE_ACKLESS;
+  }
+  route = _route;
+  ackMode = _mode;
+  timeoutLength = _timeoutLength;
+}
+
+EndpointRoute::~EndpointRoute(void){
+  delete route;
+}
+
+// base constructor, 
+Endpoint::Endpoint(
+  Vertex* _parent, String _name, 
+  EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+  boolean (*_beforeQuery)(void)
+) : Vertex(_parent, "ep_" + _name) {
+  // type, 
+	type = VT_TYPE_ENDPOINT;
+  // set callbacks,
+  if(_onData) onData_cb = _onData;
+  if(_beforeQuery) beforeQuery_cb = _beforeQuery;
+}
+
+// -------------------------------------------------------- Dummies / Defaults 
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+// -------------------------------------------------------- Endpoint Route / Write API 
+
+void Endpoint::write(uint8_t* _data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(data, _data, len);
+  dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state == EP_TX_AWAITING_ACK){
+      routes[r]->state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      routes[r]->state = EP_TX_FRESH;
+    }
+  }
+}
+
+// add a route to an endpoint, returns indice where it's dropped, 
+uint8_t Endpoint::addRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+	// guard against more-than-allowed routes 
+	if(numRoutes >= ENDPOINT_MAX_ROUTES) {
+    OSAP::error("route add is oob", MEDIUM); 
+    return 0;
+	}
+  // build, stash, increment 
+  uint8_t indice = numRoutes;
+  routes[numRoutes ++] = new EndpointRoute(_route, _mode, _timeoutLength);
+  return indice; 
+}
+
+boolean Endpoint::clearToWrite(void){
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+// -------------------------------------------------------- Loop 
+
+void Endpoint::loop(void){
+  // ok we are doing a time-based dispatch... 
+  unsigned long now = millis();
+  EndpointRoute* routeTxList[ENDPOINT_MAX_ROUTES];
+  uint8_t numTxRoutes = 0;
+  // stack fresh routes, and also transition timeouts / etc, 
+  // we make & sort this list, but set it up round-robin, since many 
+  // cases will see the same TTL & same write-to time, meaning routes that 
+  // happen to be in low indices would chance on "higher priority" 
+  uint8_t r = lastRouteServiced;
+  for(uint8_t i = 0; i < numRoutes; i ++){
+    r ++; if(r >= numRoutes) r = 0;
+    switch(routes[r]->state){
+      case EP_TX_FRESH:
+        routeTxList[numTxRoutes ++] = routes[r];
+        break;
+      case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_FRESH;
+        }
+      default:
+        // noop for IDLE / otherwise...
+        break;
+    }
+  }
+  // now, would do a sort... they're all fresh at the same time, so lowest TTL would win,
+  // this one we would want to be stable, meaning original order is preserved in 
+  // otherwise identical cases, since we round-robin fairness as well as TTL / TTD  
+  #warning no sort algo yet, 
+  // serve 'em... these are all EP_TX_FRESH state, 
+  for(r = 0; r < numTxRoutes; r ++){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // make sure we'll have enough space...
+      if(dataLen + routeTxList[r]->route->pathLen + 3 >= VT_SLOTSIZE){
+        OSAP::error("attempting to write oversized datagram at " + name, MEDIUM);
+        routeTxList[r]->state = EP_TX_IDLE;
+        continue;
+      }
+      // write dest key, mode key, & id if acked, 
+      uint16_t wptr = 0;
+      payload[wptr ++] = PK_DEST;
+      if(routeTxList[r]->ackMode == EP_ROUTEMODE_ACKLESS){
+        payload[wptr ++] = EP_SS_ACKLESS;
+      } else {
+        payload[wptr ++] = EP_SS_ACKED;
+        payload[wptr ++] = nextAckID;
+        routeTxList[r]->ackId = nextAckID;
+        nextAckID ++;
+      } 
+      // write data into the payload, 
+      memcpy(&(payload[wptr]), data, dataLen);
+      wptr += dataLen;
+      // write the packet, 
+      uint16_t len = writeDatagram(datagram, VT_SLOTSIZE, routeTxList[r]->route, payload, wptr);
+      // tx time is now, and state is awaiting ack, 
+      routeTxList[r]->lastTxTime = now;
+      routeTxList[r]->state = EP_TX_AWAITING_ACK;
+      lastRouteServiced = r;
+      // ingest it...
+      stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len);
+    } else {
+      // stack has no more empty slots, bail from the loop, 
+      break;
+    }
+  } // end fresh-tx-awaiting state checks, 
+}
+
+// -------------------------------------------------------- Destination Handler  
+
+void Endpoint::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == EP_KEY, ptr + 3 = ID (if ack req.) 
+  switch(item->data[ptr + 2]){
+    case EP_SS_ACKLESS:
+      { // singlesegment transmit-to-us, w/o ack, 
+        uint8_t* rxData = &(item->data[ptr + 3]); uint16_t rxLen = item->len - (ptr + 4);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+        switch(resp){
+          case EP_ONDATA_WAIT:    // in a wait case, we no-op / escape, it comes back around 
+            item->arrivalTime = millis();
+            break;
+          case EP_ONDATA_ACCEPT:  // here we copy it in, but carry on to the reject term to delete og gram
+            memcpy(data, rxData, rxLen);
+            dataLen = rxLen;
+          case EP_ONDATA_REJECT:  // here we simply reject it, 
+            stackClearSlot(item);
+            break;
+        } // end resp-handler, 
+      }
+      break;
+    case EP_SS_ACKED:
+      { // singlesegment transmit-to-us, w/ ack, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t* rxData = &(item->data[ptr + 4]); uint16_t rxLen = item->len - (ptr + 5);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+          switch(resp){
+            case EP_ONDATA_WAIT: // this is a little danger-danger, 
+              item->arrivalTime = millis();
+              break;
+            case EP_ONDATA_ACCEPT:
+              memcpy(data, rxData, rxLen);
+              dataLen = rxLen;
+            case EP_ONDATA_REJECT:
+              // write the ack, ship it, 
+              payload[0] = PK_DEST;
+              payload[1] = EP_SS_ACK;
+              payload[2] = id;
+              uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 3);
+              stackClearSlot(item);
+              stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+              break;
+          }
+      }
+      break;
+    case EP_QUERY:
+      {
+        // beforeQuery, 
+        beforeQuery_cb();
+        // request for our data, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_QUERY_RESP;
+        payload[2] = item->data[ptr + 3];
+        memcpy(&(payload[3]), data, dataLen);
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, dataLen + 3);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_SS_ACK:
+      // acks to us, 
+      for(uint8_t r = 0; r < numRoutes; r ++){
+        if(item->data[ptr + 3] == routes[r]->ackId){
+          switch(routes[r]->state){
+            case EP_TX_AWAITING_ACK:
+              routes[r]->state = EP_TX_IDLE;
+              goto ackEnd;
+            case EP_TX_AWAITING_AND_FRESH:
+              routes[r]->state = EP_TX_FRESH;
+              goto ackEnd;
+            case EP_TX_FRESH:
+            case EP_TX_IDLE:
+            default:
+              // these are nonsense states, likely double-transmits, likely safely ignored,
+              goto ackEnd;
+          } // end switch 
+        }
+      } // end for-each route, if we've reached this point, still dump it;
+      ackEnd:
+      stackClearSlot(item);
+      break;
+    case EP_ROUTE_QUERY_REQ:
+      // MVC request for a route of ours, 
+      {
+        uint8_t id = item->data[ptr + 3];
+        uint16_t r = ts_readUint16(item->data, ptr + 4);
+        uint16_t wptr = 0;
+        // dest, key, id... mode, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_QUERY_RES;
+        payload[wptr ++] = id;
+        if(r < numRoutes){
+          payload[wptr ++] = routes[r]->ackMode;
+          // ttl, segsize, 
+          ts_writeUint16(routes[r]->route->ttl, payload, &wptr);
+          ts_writeUint16(routes[r]->route->segSize, payload, &wptr);
+          // path ! 
+          memcpy(&(payload[wptr]), routes[r]->route->path, routes[r]->route->pathLen);
+          wptr += routes[r]->route->pathLen;
+        } else {
+          payload[wptr ++] = 0; // no-route-here, 
+        }
+        // clear request, write reply in place, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_SET_REQ:
+      // MVC request to set a new route, 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_SET_RES;
+        payload[2] = id;
+        if(numRoutes + 1 <= ENDPOINT_MAX_ROUTES){
+          // tell call-er it should work, 
+          payload[3] = 1;
+          // gather & set route, 
+          uint8_t mode = item->data[ptr + 4];
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          OSAP::debug("adding path... w/ ttl " + String(ttl) + " ss " + String(segSize) + " pathLen " + String(pathLen));
+          uint8_t routeIndice = addRoute(new Route(path, pathLen, ttl, segSize), mode);
+          payload[4] = routeIndice;
+        } else {
+          // nope, 
+          payload[3] = 0;
+          payload[4] = 0;
+        }
+        // either case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 5);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_RM_REQ:
+      // MVC request to rm a route... 
+      {
+        // msg id, & indice to remove, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t r = item->data[ptr + 4];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_RM_RES;
+        payload[2] = id;
+        if(r < numRoutes){
+          // RM ok, 
+          payload[3] = 1;
+          // delete / run destructor 
+          delete routes[r];
+          // shift...
+          for(uint8_t i = r; i < numRoutes - 1; i ++){
+            routes[i] = routes[i + 1];
+          }
+          // last is null, 
+          routes[numRoutes] = nullptr;
+          numRoutes --;
+        } else {
+          // rm not-ok
+          payload[3] = 0;
+        }
+        // either case, write reply 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 4);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    default:
+      OSAP::error("endpoint rx msg w/ unrecognized endpoint key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } // end switch... 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.h b/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.h
new file mode 100644
index 0000000000000000000000000000000000000000..b14e45a64f1346b4e034d853343a336fd75c59aa
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape/vertices/endpoint.h
@@ -0,0 +1,98 @@
+/*
+osap/vertices/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "../core/vertex.h"
+#include "../core/packets.h"
+
+// ---------------------------------------------- Endpoint Routes, extends OSAP Core Routes 
+
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+class EndpointRoute {
+  public: 
+    Route* route;
+    uint8_t ackId = 0;
+    uint8_t ackMode = EP_ROUTEMODE_ACKLESS;
+    EP_ROUTE_STATES state = EP_TX_IDLE;
+    uint32_t lastTxTime = 0;
+    uint32_t timeoutLength;
+    // constructor, 
+    EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength = 1000);
+    // destructor...
+    ~EndpointRoute(void);
+};
+
+// ---------------------------------------------- Endpoints 
+
+// endpoint handler responses must be one of these enum - 
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+// default handlers, 
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len);
+boolean beforeQueryDefault(void);
+
+class Endpoint : public Vertex {
+  public:
+    // local data store & length, 
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t dataLen = 0; 
+    // callbacks: on new data & before a query is written out 
+    EP_ONDATA_RESPONSES (*onData_cb)(uint8_t* data, uint16_t len) = onDataDefault;
+    boolean (*beforeQuery_cb)(void) = beforeQueryDefault;
+    // we override vertex loop, 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // methods,
+    void write(uint8_t* _data, uint16_t len);
+    boolean clearToWrite(void);
+    uint8_t addRoute(Route* _route, uint8_t _mode = EP_ROUTEMODE_ACKLESS, uint32_t _timeoutLength = 1000);
+    // routes, for tx-ing to:
+    EndpointRoute* routes[ENDPOINT_MAX_ROUTES];
+    uint16_t numRoutes = 0;
+    uint16_t lastRouteServiced = 0;
+    uint8_t nextAckID = 77;
+    // base constructor, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+      boolean (*_beforeQuery)(void)
+    );
+    // these are called "delegating constructors" ... best reference is 
+    // here: https://en.cppreference.com/w/cpp/language/constructor 
+    // onData only, 
+    Endpoint(   
+      Vertex* _parent, String _name,
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len)
+    ) : Endpoint ( 
+      _parent, _name, _onData, nullptr
+    ){};
+    // beforeQuery only, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      boolean (*_beforeQuery)(void)
+    ) : Endpoint (
+      _parent, _name, nullptr, _beforeQuery
+    ){};
+    // name only, 
+    Endpoint(   
+      Vertex* _parent, String _name
+    ) : Endpoint (
+      _parent, _name, nullptr, nullptr
+    ){};
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/LICENSE.md b/system/firmware/lpf-heater-module/src/osape_arduino/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/README.md b/system/firmware/lpf-heater-module/src/osape_arduino/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da4c90cb6b618b1b8206b0ddf40a240acbaa4ca7
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/README.md
@@ -0,0 +1,7 @@
+## OSAP Arduino
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+It does not do anything on its own; this one builds helper classes to turn Arduino `Serial` and `Wire` objects into *virtual ports* and *virtual busses* respectively. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.cpp b/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8634694ff54bf2f45fdc704f3fd961168f4620bb
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.cpp
@@ -0,0 +1,77 @@
+/*
+arduino-ports/vp_arduinoWire.cpp
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#include "vb_arduinoWire.h"
+
+// static stash: same per instance, 
+uint8_t stash[32];
+uint8_t stashLen = 0;
+
+VBus_ArduinoWire::VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr
+) : VBus ( _parent, _name ) {
+  wire = _wire;
+  ownRxAddr = _ownRxAddr;
+}
+
+void VBus_ArduinoWire::begin(void){
+  wire->begin(ownRxAddr);
+  wire->onReceive(this->onRecieve);
+}
+
+void VBus_ArduinoWire::onRecieve(int count){
+  Wire.readBytes(stash, count);
+  stashLen = count;
+}
+
+void VBus_ArduinoWire::loop(void){
+  // check incoming, 
+  if(stashLen > 0){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      stackLoadSlot(this, VT_STACK_ORIGIN, stash, stashLen);
+    }
+    stashLen = 0;
+  }
+}
+
+void VBus_ArduinoWire::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  digitalWrite(A1, HIGH);
+  // this'll be the big hangup, 
+  if(len > 32) return;
+  // this might guard, if we are already rx'ing... 
+  if(wire->available()) return;
+  // become host, 
+  wire->end();
+  wire->begin();
+  // transmit, 
+  wire->beginTransmission(rxAddr);
+  wire->write(data, len);
+  uint8_t res = wire->endTransmission();
+  // become guest again, 
+  wire->end();
+  wire->begin(ownRxAddr);
+  // check, 
+  //if(res != 0) 
+  // DEBUG("res " + String(res) + " txd " + String(len));
+  digitalWrite(A1, LOW);
+}
+
+boolean VBus_ArduinoWire::cts(uint8_t rxAddr){
+  return true;
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.h b/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.h
new file mode 100644
index 0000000000000000000000000000000000000000..b098634545544070b65a46e1106719567f2fbe5b
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/vb_arduinoWire.h
@@ -0,0 +1,43 @@
+/*
+arduino-ports/vp_arduinoWire.h
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#ifndef ARDU_WIRELINK_H_
+#define ARDU_WIRELINK_H_
+
+#include <Arduino.h>
+#include <Wire.h>
+#include "../osape/core/vertex.h"
+
+#define WIRELINK_BUFSIZE 255 
+
+class VBus_ArduinoWire : public VBus {
+  public:
+    void begin(void);
+    // -------------------------------- our own loop, cts, and send... 
+    void loop(void) override; 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override; 
+    boolean cts(uint8_t rxAddr) override; 
+    // -------------------------------- data 
+    TwoWire* wire;
+    static void onRecieve(int count);
+    // -------------------------------- constructors
+    VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr);
+};
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.cpp b/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f71fe57592eccba322baf9108b38d068a2aed544
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.cpp
@@ -0,0 +1,174 @@
+/*
+arduino-ports/ardu-vport.h
+
+turns serial objects into competent link layers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "vp_arduinoSerial.h"
+#include "./osape/utils/cobs.h"
+#include "../osape/core/osap.h"
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Uart* _uart
+) : VPort ( _parent, _name ){
+  stream = _uart; // should convert Uart* to Stream*, as Uart inherits stream 
+  uart = _uart; 
+}
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Serial_* _usbcdc
+) : VPort ( _parent, _name ){
+  stream = _usbcdc;
+  usbcdc = _usbcdc;
+}
+
+void VPort_ArduinoSerial::begin(uint32_t baudRate){
+  if(uart != nullptr){
+    uart->begin(baudRate);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(baudRate); 
+  }
+}
+
+void VPort_ArduinoSerial::begin(void){
+  if(uart != nullptr){
+    uart->begin(1000000);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(9600);  // baud ignored on cdc begin  
+  }
+}
+
+// link packets are max 256 bytes in length, including the 0 delimiter 
+// structured like:
+// checksum | pck/ack key | pck id | cobs encoded data | 0 
+
+void VPort_ArduinoSerial::loop(void){
+  // byte injestion: think of this like the rx interrupt stage, 
+  while(stream->available()){
+    // read byte into the current stub, 
+    rxBuffer[rxBufferWp ++] = stream->read();
+    if(rxBuffer[rxBufferWp - 1] == 0){
+      // always reset keepalive last-rx time, 
+      lastRxTime = millis();
+      // 1st, we checksum:
+      if(rxBuffer[0] != rxBufferWp){ 
+        OSAP::error("serLink bad checksum, cs: " + String(rxBuffer[0]) + " wp: " + String(rxBufferWp), MINOR);
+      } else {
+        // acks, packs, or broken things 
+        switch(rxBuffer[1]){
+          case SERLINK_KEY_PCK:
+            // dirty guard for retransmitted packets, 
+            if(rxBuffer[2] != lastIdRxd){
+              inAwaitingId = rxBuffer[2]; // stash ID 
+              inAwaitingLen = cobsDecode(&(rxBuffer[3]), rxBufferWp - 2, inAwaiting); // fill inAwaiting 
+            } else {
+              OSAP::error("serLink double rx", MINOR);
+            }
+            break;
+          case SERLINK_KEY_ACK:
+            if(rxBuffer[2] == outAwaitingId){
+              outAwaitingLen = 0;
+            }
+            break;
+          case SERLINK_KEY_KEEPALIVE:
+            // noop, 
+            break;
+          default:
+            // makes no sense, 
+            break;
+        }
+      }
+      // always reset on delimiter, 
+      rxBufferWp = 0;
+    }
+  } // end while-receive 
+
+  // check insertion & genny the ack if we can 
+  if(inAwaitingLen && stackEmptySlot(this, VT_STACK_ORIGIN) && !ackIsAwaiting){
+    stackLoadSlot(this, VT_STACK_ORIGIN, inAwaiting, inAwaitingLen);
+    ackIsAwaiting = true;
+    ackAwaiting[0] = 4;                 // checksum still, innit 
+    ackAwaiting[1] = SERLINK_KEY_ACK;   // it's an ack bruv 
+    ackAwaiting[2] = inAwaitingId;      // which pck r we akkin m8 
+    ackAwaiting[3] = 0;                 // delimiter 
+    inAwaitingLen = 0;
+  }
+
+  // check & execute actual tx 
+  checkOutputStates();
+}
+
+void VPort_ArduinoSerial::send(uint8_t* data, uint16_t len){
+  //digitalWrite(A4, !digitalRead(A4));
+  // double guard?
+  if(!cts()) return;
+  // setup, 
+  outAwaiting[0] = len + 5;               // pck[0] is checksum = len + checksum + cobs start + cobs delimit + ack/pack + id 
+  outAwaiting[1] = SERLINK_KEY_PCK;       // this ones a packet m8 
+  outAwaitingId ++; if(outAwaitingId == 0) outAwaitingId = 1;
+  outAwaiting[2] = outAwaitingId;         // an id     
+  cobsEncode(data, len, &(outAwaiting[3]));  // encode 
+  outAwaiting[len + 4] = 0;               // stuff delimiter, 
+  outAwaitingLen = outAwaiting[0];        // track... 
+  // transmit attempts etc 
+  outAwaitingNTA = 0;
+  outAwaitingLTAT = 0;
+  // try it 
+  checkOutputStates();                    // try / start write 
+}
+
+// we are CTS if outPck is not occupied, 
+boolean VPort_ArduinoSerial::cts(void){
+  return (outAwaitingLen == 0);
+}
+
+// we are open if we've heard back lately, 
+boolean VPort_ArduinoSerial::isOpen(void){
+  return (millis() - lastRxTime < SERLINK_KEEPALIVE_RX_TIME && lastRxTime != 0);
+}
+
+void VPort_ArduinoSerial::checkOutputStates(void){
+  if(ackIsAwaiting && txBufferLen == 0){   // can we ack? 
+    memcpy(txBuffer, ackAwaiting, 4);
+    txBufferLen = 4;
+    lastTxTime = millis();
+    txBufferRp = 0;
+    ackIsAwaiting = false;
+  } else if(outAwaitingLen > 0 && txBufferLen == 0){   // would we be clear to tx ? 
+    // check retransmit cases, 
+    if(outAwaitingLTAT == 0 || outAwaitingLTAT + SERLINK_RETRY_TIME < micros()){
+      memcpy(txBuffer, outAwaiting, outAwaitingLen);
+      outAwaitingLTAT = micros();
+      txBufferLen = outAwaitingLen;
+      lastTxTime = millis();
+      txBufferRp = 0;
+      outAwaitingNTA ++;
+    } 
+    // check if last attempt, 
+    if(outAwaitingNTA >= SERLINK_RETRY_MACOUNT){
+      outAwaitingLen = 0;
+    }
+  } else if (millis() - lastTxTime > SERLINK_KEEPALIVE_TX_TIME && txBufferLen == 0){
+    //OSAP::debug("keepalive-ing " + name + " " + String(isOpen()));
+    memcpy(txBuffer, keepAlivePacket, 3);
+    txBufferLen = 3;
+    lastTxTime = millis();
+  }
+  // finally, we write out so long as we can: 
+  // we aren't guaranteed to get whole pckts out in each fn call 
+  while(stream->availableForWrite() && txBufferLen != 0){
+    // output next byte, 
+    stream->write(txBuffer[txBufferRp ++]);
+    // check for end of buffer; reset transmit states if so 
+    if(txBufferRp >= txBufferLen) {
+      txBufferLen = 0; 
+      txBufferRp = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.h b/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa518aabc7e8905a85abf8ec07d4a2138b2f10f2
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_arduino/vp_arduinoSerial.h
@@ -0,0 +1,88 @@
+/*
+arduino-ports/vp_arduinoSerial.h
+
+turns arduino serial objects into competent link layers, for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ARDU_SERLINK_H_
+#define ARDU_SERLINK_H_
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+#define SERLINK_BUFSIZE 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+#define SERLINK_SEGSIZE SERLINK_BUFSIZE - 5
+// packet keys; 
+#define SERLINK_KEY_PCK 170  // 0b10101010
+#define SERLINK_KEY_ACK 171  // 0b10101011
+#define SERLINK_KEY_KEEPALIVE 173 
+// retry settings 
+#define SERLINK_RETRY_MACOUNT 2
+#define SERLINK_RETRY_TIME 100000  // microseconds 
+#define SERLINK_KEEPALIVE_TX_TIME 800 // milliseconds 
+#define SERLINK_KEEPALIVE_RX_TIME 1200 // ms 
+
+#define SERLINK_LIGHT_ON_TIME 100 // in ms 
+
+// note that we use uint8_t write ptrs / etc: and a size of 255, 
+// so we are never dealing w/ wraps etc, god bless 
+
+class VPort_ArduinoSerial : public VPort {
+  public:
+    // arduino std begin 
+    void begin(uint32_t baud);
+    void begin(void);
+    // -------------------------------- our own gd send & cts & loop fns, 
+    void loop(void) override;
+    void checkOutputStates(void);
+    void send(uint8_t* data, uint16_t len) override;
+    boolean cts(void) override;
+    boolean isOpen(void) override;
+    // -------------------------------- Data 
+    // Uart & USB are both Stream classes, 
+    Stream* stream;
+    // we have an overloaded constructor w/ uart or Serial_, the usb class 
+    Uart* uart = nullptr;
+    Serial_* usbcdc = nullptr; 
+    // incoming, always kept clear to receive: 
+    uint8_t rxBuffer[SERLINK_BUFSIZE];
+    uint8_t rxBufferWp = 0;
+    // keepalive state, 
+    uint32_t lastRxTime = 0;
+    uint32_t lastTxTime = 0;
+    uint8_t keepAlivePacket[3] = {3, SERLINK_KEY_KEEPALIVE, 0};
+    // guard on double transmits 
+    uint8_t lastIdRxd = 0;
+    // incoming stash
+    uint8_t inAwaiting[SERLINK_BUFSIZE];
+    uint8_t inAwaitingId = 0;
+    uint8_t inAwaitingLen = 0;
+    // outgoing ack, 
+    uint8_t ackAwaiting[4];
+    boolean ackIsAwaiting = false;
+    // outgoing await,
+    uint8_t outAwaiting[SERLINK_BUFSIZE];
+    uint8_t outAwaitingId = 1;
+    uint8_t outAwaitingLen = 0;
+    uint8_t outAwaitingNTA = 0;
+    unsigned long outAwaitingLTAT = 0;
+    // outgoing buffer,
+    uint8_t txBuffer[SERLINK_BUFSIZE];
+    uint8_t txBufferLen = 0;
+    uint8_t txBufferRp = 0;
+    // -------------------------------- Constructors 
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Uart* _uart);
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Serial_* _usbcdc);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/README.md b/system/firmware/lpf-heater-module/src/osape_ucbus/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e5a9fae5795a46730372cd9533efa958bc12c2e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/README.md
@@ -0,0 +1,6 @@
+## UART-Clocked Bus Submodule 
+
+https://gitlab.cba.mit.edu/jakeread/ucbus 
+https://github.com/jakeread/ucbus 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.cpp b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2bd443fa0154b4e62e4392611b7afabb257fb
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.cpp
@@ -0,0 +1,510 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "ucBusDipConfig.h"
+#include "../indicators.h"
+#include "../osape/core/osap.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+// last-time-rx'd 
+volatile uint32_t lastRxTime = 0;
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts: not the highest priority (0), just under. 
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  NVIC_SetPriority(SERCOM1_IRQn, 1);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // track keepalive 
+    lastRxTime = millis();
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { 
+    OSAP::error("ucbus-drop outsize numToken rx", MINOR); 
+    return; 
+  }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    OSAP::error("ucbus-drop rx overfull buffer", MINOR);
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || rxCh == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        OSAP::error("ucbus-drop rx FC fails on ch " + String(rxCh), MINOR);
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        // ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        // inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+boolean ucBusDrop_ctrA(void){
+  // likewise
+  return (inBufferLen[0] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+size_t ucBusDrop_readA(uint8_t* dest){
+  if(!ucBusDrop_ctrA()) return 0;
+  // we read out the whole gd thing,
+  size_t len = inBufferLen[0];
+  memcpy(dest, &(inBuffer[0]), len);
+  inBufferLen[0] = 0; // now empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusDrop_isPresent(uint8_t drop){
+  // can't tx anywhere other than to head, 
+  if(drop > 0) return false;
+  return (millis() - lastRxTime < UB_KEEPALIVE_TIME);
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.h b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..281f430bd6ced5264fd9607ce8f51a1a9c31cbab
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusDrop.h
@@ -0,0 +1,51 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t* dest);
+boolean ucBusDrop_ctrA(void);
+size_t ucBusDrop_readA(uint8_t* dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+boolean ucBusDrop_isPresent(uint8_t rxAddr);
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.cpp b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..854e488395920dd19b812643282ddbf9c7f3ae25
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.cpp
@@ -0,0 +1,386 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../osape/core/osap.h"
+#include "./utils_samd51/peripheral_nums.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// last-rx'd-time, per drop presence-detect, 
+volatile uint32_t lastRxTime[UB_MAX_DROPS];
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin, also set lastRxTime to zero for each, 
+  for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+    lastRxTime[d] = 0;
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      outBufferLen[ch] = 0;
+      outBufferRp[ch] = 0;
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (channel '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] || ch == 0)){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update keepalive: last we heard from this drop:
+    lastRxTime[rxDrop] = millis();
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { 
+      OSAP::error("ucbus-head outsize numToken rx", MEDIUM); 
+      return; 
+    }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ 
+      OSAP::error("ucbus-head rx FC broken", MEDIUM); 
+      return; 
+    }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      OSAP::error("ucbus-head rx packet too-long", MEDIUM);
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero / are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusHead_isPresent(uint8_t drop){
+  if(drop > UCBUS_MAX_DROPS) return false;
+  return (millis() - lastRxTime[drop] < UB_KEEPALIVE_TIME);
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: channel ID
+  outBuffer[0][0] = channel;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.h b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..65f43edcfc482f9656fe30d0bf7f7ea0f9c1eb67
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+boolean ucBusHead_isPresent(uint8_t drop); // have we heard from this drop recently ? 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusMacros.h b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusMacros.h
new file mode 100644
index 0000000000000000000000000000000000000000..72f3f0c0b60e6c7efac390db2e6db4be7e9b133a
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucBusMacros.h
@@ -0,0 +1,127 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "./ucbus_config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// time-until-considered-dead, in ms  
+#define UB_KEEPALIVE_TIME 200 
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL 2 // A: 0, B: 1, C: 2
+#define UB_RXPERIPHERAL 2
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.cpp b/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08742fdc5cda9435f8ad54b76a4f85c81c433b1e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.h b/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..97ec2b5750e86bbd6d98acd3ef42c02b489240f4
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.cpp b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a49901b40751839e972e4f5d778179834dba1868
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.cpp
@@ -0,0 +1,95 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusDrop.h"
+#include "../osape/core/osap.h"
+
+// badness, direct write in future 
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusDrop::VBus_UCBusDrop(Vertex* _parent, String _name
+): VBus(_parent, _name){
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusDrop::begin(void){
+  ucBusDrop_setup(true, 0);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::begin(uint8_t _ownRxAddr){
+  ucBusDrop_setup(false, _ownRxAddr);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::loop(void){
+  // can we shift-in from channel a / broadcast messages ?
+  // also... stack 'em from the broadcast channel first, typically higher priority 
+  if(ucBusDrop_ctrA()){
+    // and if we have an empty space... 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+    // get len & strip out the broadcastChannel, which was stuffed at [0]
+    uint16_t len = ucBusDrop_readA(_tempBuffer);
+    injestBroadcastPacket(&(_tempBuffer[1]), len - 1, _tempBuffer[0]);
+    }
+  }
+  // can we shift-in from channel b / directed messages ? 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+void VBus_UCBusDrop::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    OSAP::error("ubd tx while not clear", MEDIUM);
+  }
+}
+
+boolean VBus_UCBusDrop::cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  return (rxAddr == 0 && ucBusDrop_ctsB());
+}
+
+void VBus_UCBusDrop::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  OSAP::debug("Broadcast is unwritten");
+}
+
+boolean VBus_UCBusDrop::ctb(uint8_t broadcastChannel){
+  OSAP::debug("Bus Drop CTB is unwritten");
+  return false;
+}
+
+boolean VBus_UCBusDrop::isOpen(uint8_t rxAddr){
+  return ucBusDrop_isPresent(rxAddr);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.h b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4333e6491b0439d01ae4bc480bce37af864f5
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusDrop.h
@@ -0,0 +1,41 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VBUS_UCBUS_HEAD_H_
+#define VBUS_UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusDrop : public VBus {
+  public:
+    void begin(void);
+    void begin(uint8_t _ownRxAddr);
+    void loop(void) override;
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr);
+    VBus_UCBusDrop(Vertex* _parent, String _name);
+};
+
+#endif 
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.cpp b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0e5cd5676e138fa0c17215c075181594ff48ac
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.cpp
@@ -0,0 +1,93 @@
+/*
+osap/vb_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusHead.h"
+#include "../osape/core/osap.h"
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusHead::VBus_UCBusHead(Vertex* _parent, String _name
+): VBus (_parent, _name) {
+  // report our address size,
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusHead::begin(void){
+  // start ucbus
+  ucBusHead_setup(); 
+}
+
+void VBus_UCBusHead::loop(void){
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      if (stackEmptySlot(this, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+void VBus_UCBusHead::timerISR(void){
+  ucBusHead_timerISR();
+}
+
+void VBus_UCBusHead::send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    OSAP::error("attempt to busf from head to self", MEDIUM);
+  } else {  
+    ucBusHead_transmitB(data, len, rxAddr);
+  }
+}
+
+boolean VBus_UCBusHead::cts(uint8_t rxAddr){
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void VBus_UCBusHead::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  ucBusHead_transmitA(data, len, broadcastChannel);
+}
+
+boolean VBus_UCBusHead::ctb(uint8_t broadcastChannel){
+  return ucBusHead_ctsA();
+}
+
+boolean VBus_UCBusHead::isOpen(uint8_t rxAddr){
+  return ucBusHead_isPresent(rxAddr);
+}
+
+#endif 
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.h b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfb7829f135f8ea04f193d3657f38cb15ea63cfa
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/osape_ucbus/vb_ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/vb_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusHead : public VBus {
+  public:
+    void begin(void);
+    // loop to ferry data, 
+    void loop(void) override;
+    // fast loop, needs to be called in ~ 10kHz ISR 
+    void timerISR(void);
+    // ... bus : osap API 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr) override;
+    // -------------------------------- Constructors 
+    VBus_UCBusHead(Vertex* _parent, String _name);
+};
+
+#endif
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/thermalConfig.h b/system/firmware/lpf-heater-module/src/thermalConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..478dd3ff66297ab4ec7649512cfeb8e7b6e3cd8e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/thermalConfig.h
@@ -0,0 +1,80 @@
+#ifndef THERMAL_CONFIG_H_
+#define THERMAL_CONFIG_H_
+
+// hotend, or hot bed? 
+// on both, THERM_A is on PA04 / arduino A4 
+// also on both, GATE_A is on PA22 / TC4, 
+// difference: hotbed has RTD1K therm, so linearization is different 
+#define IS_HOTEND
+// #define IS_HOTBED
+
+#ifdef IS_HOTBED
+
+#define MAX_PWM 1.0F
+#define TABLE_SIZE 16
+#define TABLE_DIR_UP false
+// reading temps... spreadsheet & table in the log
+// for PN 478-10979-1-ND
+float voltages[TABLE_SIZE] = {
+  3.29972, 3.29866, 3.29498, 3.29099, 3.28448,
+  3.27428, 3.25884, 3.23625, 3.20422, 3.16020,
+  3.10154, 3.02574, 2.81573, 2.52731, 2.17958,
+  1.80939, 
+};
+
+float temps[TABLE_SIZE] = {
+  -50, -30, -10, 0, 10,
+  20, 30, 40, 50, 60,
+  70, 80, 100, 120, 140,
+  160,
+};
+
+/*
+// the below is config for the RTD 223-1563-1-ND
+#define TABLE_SIZE 23
+#define TABLE_DIR_UP true 
+// reading temps... spreadsheet & table in the log
+float voltages[TABLE_SIZE] = {
+  1.35333, 1.40628, 1.45776, 1.50798, 1.55674,
+  1.60334, 1.65000, 1.69455, 1.73780, 1.77968,
+  1.82025, 1.85964, 1.89777, 1.93478, 1.97069,
+  2.00553, 2.03940, 2.07228, 2.10422, 2.13528,
+  2.16555, 2.19495, 2.22364
+};
+
+float temps[TABLE_SIZE] = {
+  -60, -50, -40, -30, -20,
+  -10, 0, 10, 20, 30,
+  40, 50, 60, 70, 80,
+  90, 100, 110, 120, 130,
+  140, 150, 160
+};
+*/
+
+#endif
+
+#ifdef IS_HOTEND
+
+#define MAX_PWM 1.0F
+#define TABLE_SIZE 24
+#define TABLE_DIR_UP false
+// reading temps... spreadsheet & table in the log
+float voltages[TABLE_SIZE] = {
+  3.29963, 3.29847, 3.29471, 3.29072, 3.28427,
+  3.26733, 3.23642, 3.20421, 3.15945, 3.02034,
+  2.97307, 2.79764, 2.48679, 2.10866, 1.70887,
+  1.33138, 1.01658, 0.76466, 0.57363, 0.43218,
+  0.32837, 0.25229, 0.20141, 0.0,
+};
+
+float temps[TABLE_SIZE] = {
+  -50, -30, -10, 0, 10,
+  25, 40, 50, 60, 80,
+  85, 100, 120, 140, 160,
+  180, 200, 220, 240, 260,
+  280, 300, 320, 400,
+};
+
+#endif
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/ucbus_config.h b/system/firmware/lpf-heater-module/src/ucbus_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..eee24b0289a8649bc3543cd3dc759ebd0b131d97
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/ucbus_config.h
@@ -0,0 +1,29 @@
+/*
+ucbus_confi.h
+
+config options for an ucbus instance 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_CONFIG_H_
+#define UCBUS_CONFIG_H_
+
+#define UCBUS_MAX_DROPS 32 
+#define UCBUS_IS_DROP 
+//#define UCBUS_IS_HEAD 
+
+#define UCBUS_BAUD 2 
+
+#define UCBUS_IS_D51
+// #define UCBUS_IS_D21
+
+#define UCBUS_ON_OSAP 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/utils_samd51/README.md b/system/firmware/lpf-heater-module/src/utils_samd51/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a5e4922e5be8001ad756c57dc6cd5c934ca1572e
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/utils_samd51/README.md
@@ -0,0 +1,3 @@
+## ATSAMD51 Utes
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.cpp b/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387cbaad46e7f17a5f553c446a47558abbc20bc7
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.cpp
@@ -0,0 +1,129 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "clock_utils.h"
+#include "../indicators.h"
+
+/*
+// I used to have this singleton stuff here, but I think
+// since I am using the extern... I have no need for it, 
+D51ClockUtils* D51ClockUtils::instance = 0;
+
+D51ClockUtils* D51ClockUtils::getInstance(void){
+    if(instance == 0){
+        instance = new D51ClockUtils();
+    }
+    return instance;
+}
+
+D51ClockUtils* D51ClockUtils = D51ClockUtils::getInstance();
+*/
+
+D51ClockUtils* d51ClockUtils;
+
+D51ClockUtils::D51ClockUtils(){}
+
+void D51ClockUtils::setup_16mhz_xtal(void){
+    if(mhz_xtal_is_setup) return; // already done, 
+    // let's make a clock w/ that xtal:
+    OSCCTRL->XOSCCTRL[0].bit.RUNSTDBY = 0;
+    OSCCTRL->XOSCCTRL[0].bit.XTALEN = 1;
+    // set oscillator current..
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_IMULT(4) | OSCCTRL_XOSCCTRL_IPTAT(3);
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_STARTUP(5);
+    OSCCTRL->XOSCCTRL[0].bit.ENALC = 1;
+    OSCCTRL->XOSCCTRL[0].bit.ENABLE = 1;
+    // make the peripheral clock available on this ch 
+    GCLK->GENCTRL[MHZ_XTAL_GCLK_NUM].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_XOSC0) | GCLK_GENCTRL_GENEN;  // GCLK_GENCTRL_SRC_DFLL
+    while (GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(MHZ_XTAL_GCLK_NUM)){
+        //DEBUG2PIN_TOGGLE;
+    };
+    mhz_xtal_is_setup = true;
+}
+
+void D51ClockUtils::start_ticker_a(uint32_t us){
+    //now using 120mHz main clock (gen(0)) instead of xtal, 
+    //setup_16mhz_xtal();
+    // ok
+    TC0->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC1->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0 | MCLK_APBAMASK_TC1;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC0->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC1->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC0->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC1->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC0->COUNT32.INTENSET.bit.MC0 = 1;
+    TC0->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC0->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 100kHz, ok? 
+    if(us < 10) us = 10;
+    // 120 / 2 -> 60 ticks per us, 
+    TC0->COUNT32.CC[0].reg = 60 * us;
+    // enable, sync for enable write
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC0->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC1->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC0_IRQn);
+    NVIC_SetPriority(TC0_IRQn, 2);
+}
+
+void D51ClockUtils::set_ticker_a_priority(uint32_t prio){
+    if(prio > 3) prio = 3;
+    NVIC_SetPriority(TC0_IRQn, prio);
+}
+
+void D51ClockUtils::start_ticker_b(uint32_t us){
+    setup_16mhz_xtal();
+    // ok
+    TC2->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC3->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBBMASK.reg |= MCLK_APBBMASK_TC2 | MCLK_APBBMASK_TC3;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC2_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC3_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC2->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC3->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC2->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC3->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC2->COUNT32.INTENSET.bit.MC0 = 1;
+    TC2->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC2->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 1MHz, ok? 
+    if(us < 8) us = 8;
+    TC2->COUNT32.CC[0].reg = 8 * us;
+    // enable, sync for enable write
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC2->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC3->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC2_IRQn);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.h b/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1f9e7472c034dc3a1aee6fc995eb65043d660
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/utils_samd51/clock_utils.h
@@ -0,0 +1,45 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef CLOCKS_D51_H_
+#define CLOCKS_D51_H_
+
+#include <Arduino.h>
+
+#define MHZ_XTAL_GCLK_NUM 9
+
+class D51ClockUtils {
+    private:
+        static D51ClockUtils* instance;
+    public:
+        D51ClockUtils();
+        static D51ClockUtils* getInstance(void);
+        // xtal
+        volatile boolean mhz_xtal_is_setup = false;
+        uint32_t mhz_xtal_gclk_num = 9;
+        void setup_16mhz_xtal(void);
+        // uses TC0 and TC1 as 32 bit TC
+        // pickup TC0_Handler(void){}
+        // do in handler: 
+        // TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+        // TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+        // us: requested timer period 
+        void start_ticker_a(uint32_t us);
+        void set_ticker_a_priority(uint32_t prio);
+        // uses TC2 and TC3 as 32 bit TC 
+        // pickup on TC2_Handler(void){}
+        void start_ticker_b(uint32_t us);
+};
+
+extern D51ClockUtils* d51ClockUtils;
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/utils_samd51/peripheral_nums.h b/system/firmware/lpf-heater-module/src/utils_samd51/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/utils_samd51/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/src/utils_samd51/pin_macros.h b/system/firmware/lpf-heater-module/src/utils_samd51/pin_macros.h
new file mode 100644
index 0000000000000000000000000000000000000000..89418657d8a481cb20ec3532cbd3ef0488dda521
--- /dev/null
+++ b/system/firmware/lpf-heater-module/src/utils_samd51/pin_macros.h
@@ -0,0 +1,13 @@
+#ifndef PIN_MACROS_D51_H_
+#define PIN_MACROS_D51_H_
+
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+#define PIN_SETUP_INPUT(port, pin) PORT->Group[port].DIRCLR.reg = PIN_BM(pin); PORT->Group[port].PINCFG[pin].reg = PORT_PINCFG_INEN
+#define PIN_SETUP_PULLEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PULLEN = 1
+#define PIN_SETUP_PMUXEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PMUXEN = 1
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-heater-module/test/README b/system/firmware/lpf-heater-module/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..b94d0890faa00a63737892509a5ca77ad3bdc6c3
--- /dev/null
+++ b/system/firmware/lpf-heater-module/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Unit Testing and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/page/plus/unit-testing.html
diff --git a/system/firmware/lpf-loadcell-amp/.gitignore b/system/firmware/lpf-loadcell-amp/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/system/firmware/lpf-loadcell-amp/.vscode/extensions.json b/system/firmware/lpf-loadcell-amp/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/system/firmware/lpf-loadcell-amp/include/README b/system/firmware/lpf-loadcell-amp/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/system/firmware/lpf-loadcell-amp/lib/README b/system/firmware/lpf-loadcell-amp/lib/README
new file mode 100644
index 0000000000000000000000000000000000000000..6debab1e8b4c3faa0d06f4ff44bce343ce2cdcbf
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/system/firmware/lpf-loadcell-amp/platformio.ini b/system/firmware/lpf-loadcell-amp/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..50208f0cadb2e924ce3a661e45b5c88422dc71af
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/platformio.ini
@@ -0,0 +1,14 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:adafruit_feather_m4]
+platform = atmelsam
+board = adafruit_feather_m4
+framework = arduino
diff --git a/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.cpp b/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e8b6406ef1a2bdd88e0b8a1484b59c28c477efce
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.cpp
@@ -0,0 +1,95 @@
+/*
+drivers/ads1231.cpp
+
+reads TI ADS1231 loadcell amplifier 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ads1231.h"
+
+ADS_1231* ADS_1231::instance = 0;
+
+ADS_1231* ADS_1231::getInstance(void){
+    if(instance == 0){
+        instance = new ADS_1231();
+    }
+    return instance;
+}
+
+ADS_1231* ads_1231 = ADS_1231::getInstance();
+
+ADS_1231::ADS_1231(void){}
+
+uint32_t misoPins[3] = {11, 15, 7};
+uint32_t clkPins[3] = {8, 13, 5};
+uint32_t pinPorts[3] = {0, 1, 0};
+
+void ADS_1231::init(void){
+    // there's one mode pin on the board, parallel to all three channels:
+    // SPEED (mode pin) on PB16: set high for 80 Hz, low for 10 Hz
+    // fair warning: I've seen some erratic behaviour at 80 Hz 
+    PORT->Group[1].DIRSET.reg = (1 << 16);
+    PORT->Group[1].OUTCLR.reg = (1 << 16);
+    // and one power-down pin, we just turn the things on all the time: 
+    // PDWN on PB22: low to reset, high to enable 
+    PORT->Group[1].DIRSET.reg = (1 << 22);
+    PORT->Group[1].OUTSET.reg = (1 << 22);
+    // one clock mode pin, same net for each channel as well: 
+    // CLKIN on PB17 (either external src ~ 4Mhz or tie low for internal osc)
+    PORT->Group[1].DIRSET.reg = (1 << 17);
+    PORT->Group[1].OUTCLR.reg = (1 << 17);
+    // now each channel has an independent CLK and MISO line,
+    for(uint8_t i = 0; i < 3; i ++){
+        // dataRead / dataOut (MISO) pin: goes low when data ready, then is dout on clk edge 
+        PORT->Group[pinPorts[i]].DIRCLR.reg = (1 << misoPins[i]);
+        PORT->Group[pinPorts[i]].PINCFG[misoPins[i]].bit.INEN = 1;
+        // and clks are outputs:
+        PORT->Group[pinPorts[i]].DIRSET.reg = (1 << clkPins[i]);
+        PORT->Group[pinPorts[i]].OUTCLR.reg = (1 << clkPins[i]);
+    }
+}
+
+#define DRDY_HIGH(ch) PORT->Group[pinPorts[ch]].IN.reg & (1 << misoPins[ch])
+#define CLK_HIGH(ch) PORT->Group[pinPorts[ch]].OUTSET.reg = (1 << clkPins[ch])
+#define CLK_LOW(ch) PORT->Group[pinPorts[ch]].OUTCLR.reg = (1 << clkPins[ch])
+
+void ADS_1231::loop(void){
+    // hopefully this catches each fast enough (?)
+    if(!(DRDY_HIGH(0)) && !(DRDY_HIGH(1)) && !(DRDY_HIGH(2))){
+        for(uint8_t ch = 0; ch < 3; ch ++){
+            // make sure clk is low, 
+            CLK_LOW(ch);
+            delayMicroseconds(1);
+            // do 24 clock pulses... one per bit 
+            int32_t reading = 0;
+            for(uint8_t i = 0; i < 24; i ++){
+                CLK_HIGH(ch);
+                delayMicroseconds(1); // clock delay on datasheet is only 50ns, 
+                if(DRDY_HIGH(ch)){
+                    reading |= (1 << (23 - i));
+                }
+                CLK_LOW(ch);
+                delayMicroseconds(1);
+            }
+            // fill in leading zeros for -ve vals 
+            if(reading & (1 << 23)){
+                for(uint8_t i = 0; i < 8; i ++){
+                    reading |= (1 << (24 + i));
+                }
+            }
+            // now one more pulse to reset the DRDY to high
+            CLK_HIGH(ch);
+            delayMicroseconds(1);
+            CLK_LOW(ch);
+            // done here
+            latest[ch] = reading;
+        }
+    }
+}
diff --git a/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.h b/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.h
new file mode 100644
index 0000000000000000000000000000000000000000..50bfce595c9f395ee6ca8c180f62979348eb4cf3
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/drivers/ads1231.h
@@ -0,0 +1,33 @@
+/*
+drivers/ads1231.h
+
+reads TI ADS1231 loadcell amplifier 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ADS_1231_H_
+#define ADS_1231_H_
+
+#include <Arduino.h>
+
+class ADS_1231 {
+    private:
+        static ADS_1231* instance;
+    public:
+        ADS_1231(void);
+        static ADS_1231* getInstance(void);
+        void init(void);
+        void loop(void);
+        int32_t latest[3] = {0,0,0};
+};
+
+extern ADS_1231* ads_1231;
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/indicators.h b/system/firmware/lpf-loadcell-amp/src/indicators.h
new file mode 100644
index 0000000000000000000000000000000000000000..b8151b2fa843b7b243e9445a02a44b35a30b7494
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/indicators.h
@@ -0,0 +1,46 @@
+// indicators for the module (most recent version ... with extension pins, thru hole swd)
+#define CLKLIGHT_PIN 27
+#define CLKLIGHT_PORT PORT->Group[0]
+#define ERRLIGHT_PIN 8
+#define ERRLIGHT_PORT PORT->Group[1]
+// debug for the loadcell amp:
+// these are the CI / CK / MI pins
+#define DEBUG1PIN_PIN 17
+#define DEBUG1PIN_PORT PORT->Group[1]
+#define DEBUG2PIN_PIN 5
+#define DEBUG2PIN_PORT PORT->Group[0]
+#define DEBUG3PIN_PIN 7
+#define DEBUG3PIN_PORT PORT->Group[0]
+
+#define CLKLIGHT_BM (uint32_t)(1 << CLKLIGHT_PIN)
+#define CLKLIGHT_ON CLKLIGHT_PORT.OUTCLR.reg = CLKLIGHT_BM
+#define CLKLIGHT_OFF CLKLIGHT_PORT.OUTSET.reg = CLKLIGHT_BM
+#define CLKLIGHT_TOGGLE CLKLIGHT_PORT.OUTTGL.reg = CLKLIGHT_BM
+#define CLKLIGHT_SETUP CLKLIGHT_PORT.DIRSET.reg = CLKLIGHT_BM; CLKLIGHT_OFF
+
+#define ERRLIGHT_BM (uint32_t)(1 << ERRLIGHT_PIN)
+#define ERRLIGHT_ON ERRLIGHT_PORT.OUTCLR.reg = ERRLIGHT_BM
+#define ERRLIGHT_OFF ERRLIGHT_PORT.OUTSET.reg = ERRLIGHT_BM
+#define ERRLIGHT_TOGGLE ERRLIGHT_PORT.OUTTGL.reg = ERRLIGHT_BM
+#define ERRLIGHT_SETUP ERRLIGHT_PORT.DIRSET.reg = ERRLIGHT_BM; ERRLIGHT_OFF
+
+/*
+
+#define DEBUG1PIN_BM (uint32_t)(1 << DEBUG1PIN_PIN)
+#define DEBUG1PIN_ON DEBUG1PIN_PORT.OUTSET.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_OFF DEBUG1PIN_PORT.OUTCLR.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_TOGGLE DEBUG1PIN_PORT.OUTTGL.reg = DEBUG1PIN_BM
+#define DEBUG1PIN_SETUP DEBUG1PIN_PORT.DIRSET.reg = DEBUG1PIN_BM; DEBUG1PIN_OFF
+
+#define DEBUG2PIN_BM (uint32_t)(1 << DEBUG2PIN_PIN)
+#define DEBUG2PIN_ON DEBUG2PIN_PORT.OUTSET.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_OFF DEBUG2PIN_PORT.OUTCLR.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_TOGGLE DEBUG2PIN_PORT.OUTTGL.reg = DEBUG2PIN_BM
+#define DEBUG2PIN_SETUP DEBUG2PIN_PORT.DIRSET.reg = DEBUG2PIN_BM; DEBUG2PIN_OFF
+
+#define DEBUG3PIN_BM (uint32_t)(1 << DEBUG3PIN_PIN)
+#define DEBUG3PIN_ON DEBUG3PIN_PORT.OUTSET.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_OFF DEBUG3PIN_PORT.OUTCLR.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_TOGGLE DEBUG3PIN_PORT.OUTTGL.reg = DEBUG3PIN_BM
+#define DEBUG3PIN_SETUP DEBUG3PIN_PORT.DIRSET.reg = DEBUG3PIN_BM; DEBUG3PIN_OFF
+*/
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/main.cpp b/system/firmware/lpf-loadcell-amp/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a9bb9da778e1236234a4b6d2d2884d29dbe6265f
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/main.cpp
@@ -0,0 +1,126 @@
+#include <Arduino.h>
+#include "indicators.h"
+
+#include "osape/core/osap.h"
+#include "osape/vertices/endpoint.h"
+#include "osape_arduino/vp_arduinoSerial.h"
+#include "osape_ucbus/vb_ucBusDrop.h"
+
+// loadcell 
+#include "drivers/ads1231.h"
+
+// -------------------------------------------------------- OSAP 
+
+OSAP osap("loadcell-amp");
+
+// -------------------------------------------------------- VPORTS 
+
+VPort_ArduinoSerial vpUSBSerial(&osap, "arduinoUSBSerial", &Serial);
+
+VBus_UCBusDrop vbUCBusDrop(&osap, "ucBusDrop");
+
+// -------------------------------------------------------- ENDPOINTS 
+
+// -------------------------------------------------------- Read Loadcell: 2
+
+Endpoint loadcellReadEP(&osap, "loadcells");
+
+// -------------------------------------------------------- Comparator: 3 
+
+boolean comparePositiveEdge = true;
+int32_t lastValue = 0;
+int32_t compareValue = 0;
+
+EP_ONDATA_RESPONSES onComparatorData(uint8_t* data, uint16_t len){
+  comparePositiveEdge = data[0] ? true : false;
+  uint16_t rptr = 1;
+  compareValue = ts_readInt32(data, &rptr);
+  // OSAP::debug("compare positive " + String(comparePositiveEdge) + " to val " + String(compareValue));
+  return EP_ONDATA_REJECT;
+}
+
+Endpoint comparatorEP(&osap, "loadcell-comparator", onComparatorData);
+
+// -------------------------------------------------------- "Driver" - update states & talk to comparator 
+
+uint8_t datagram[12];
+uint32_t lastErrLightOnTime = 0;
+
+void updateStates(void){
+  // read lc channels & publish, 
+  chunk_int32 rd[3] = {
+      { i: ads_1231->latest[0] },
+      { i: ads_1231->latest[1] },
+      { i: ads_1231->latest[2] }
+    };
+  memcpy(datagram, rd[0].bytes, 4);
+  memcpy(&(datagram[4]), rd[1].bytes, 4);
+  memcpy(&(datagram[8]), rd[2].bytes, 4);  
+  loadcellReadEP.write(datagram, 12);
+  // check comparator:
+  datagram[0] = 1;
+  int32_t val = ads_1231->latest[0];
+  //OSAP::debug(String(val));
+  if(comparePositiveEdge){
+    if(val > compareValue && lastValue < compareValue){
+      //OSAP::debug("firing comp");
+      ERRLIGHT_ON;
+      lastErrLightOnTime = millis();
+      comparatorEP.write(datagram, 1);
+    }
+  } else {
+    if(val < compareValue && lastValue > compareValue){
+      //OSAP::debug("firing comp");
+      ERRLIGHT_ON;
+      lastErrLightOnTime = millis();
+      comparatorEP.write(datagram, 1);
+    }
+  }
+  lastValue = val;
+}
+
+// -------------------------------------------------------- SETUP / RUN 
+
+void setup() {
+  CLKLIGHT_SETUP;
+  ERRLIGHT_SETUP;
+  // setup comms 
+  vpUSBSerial.begin();
+  vbUCBusDrop.begin();
+  // startup loadcell 
+  ads_1231->init();
+}
+
+// -------------------------------------------------------- LOOP 
+
+uint32_t lastUpdate = 0;
+uint32_t updatePerioduS = 12500; // loadcell amp can do 80hz at best 
+
+uint32_t lastBlink = 0;
+uint32_t blinkPeriod = 250;
+
+void loop() {
+  osap.loop();
+  ads_1231->loop();
+  if(lastUpdate + updatePerioduS < micros()){
+    lastUpdate = micros();
+    updateStates();
+  }
+  if(lastBlink + blinkPeriod < millis()){
+    lastBlink = millis();
+    CLKLIGHT_TOGGLE;
+  }
+  if(lastErrLightOnTime + 250 < millis()){
+    ERRLIGHT_OFF;
+  }
+}
+
+// -------------------------------------------------------- HANDLES
+
+void ucBusDrop_onRxISR(void){
+
+}
+
+void ucBusDrop_onPacketARx(uint8_t* data, uint16_t len){
+
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osap_config.h b/system/firmware/lpf-loadcell-amp/src/osap_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..f94ddc11991022908e22357c21e15e17a03fd82f
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osap_config.h
@@ -0,0 +1,34 @@
+/*
+osap_config.h
+
+config options for an osap-embedded build 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_CONFIG_H_
+#define OSAP_CONFIG_H_
+
+// size of vertex stacks, lenght, then count,
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 3  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+#define VT_MAXITEMSPERTURN 8
+
+// max # of endpoints that could be spawned here,
+#define MAX_CONTEXT_ENDPOINTS 64
+
+// count of routes each endpoint can have, 
+#define ENDPOINT_MAX_ROUTES 4
+#define ENDPOINT_ROUTE_MAX_LEN 64 
+
+// count of broadcast channels width, 
+#define VBUS_MAX_BROADCAST_CHANNELS 64 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/LICENSE.md b/system/firmware/lpf-loadcell-amp/src/osape/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/README.md b/system/firmware/lpf-loadcell-amp/src/osape/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c94ebaff92a9980dbc93aa25047846ee4aa64e0
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/README.md
@@ -0,0 +1,5 @@
+## OSAP Embedded 
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/loop.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/loop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c050974467d2fc95677d72f2e2da3b6608a0f588
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/loop.cpp
@@ -0,0 +1,255 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "loop.h"
+#include "packets.h"
+#include "osap.h"
+
+#define MAX_ITEMS_PER_LOOP 32
+//#define LOOP_DEBUG
+
+// we'll stack up to 64 messages to handle per loop, 
+// more items would cause issues: will throw errors and design circular looping at that point 
+stackItem* itemList[MAX_ITEMS_PER_LOOP];
+uint16_t itemListLen = 0;
+
+void listSetupRecursor(Vertex* vt){
+  // run the vertex' loop... but not if it's the root, yar 
+  if(vt->type != VT_TYPE_ROOT) vt->loop();
+  // for each input / output stack, try to collect all items... 
+  // alright I'm doing this collect... but want a kind of pickup-where-you-left-off thing, 
+  // so that we can have a fixed-length loop, i.e. 64 items per, but still do fairness... 
+  // otherwise our itemList has to be large enough to carry potentially every single item ? 
+  for(uint8_t od = 0; od < 2; od ++){
+    uint8_t count = stackGetItems(vt, od, &(itemList[itemListLen]), MAX_ITEMS_PER_LOOP - itemListLen);
+    itemListLen += count;
+  }
+  // recurse children...
+  for(uint8_t c = 0; c < vt->numChildren; c ++){
+    listSetupRecursor(vt->children[c]);
+  }
+}
+
+// sort-in-place based on time-to-death, 
+void listSort(stackItem** list, uint16_t listLen){
+  // write each item's time-to-death, 
+  uint32_t now = millis();
+  for(uint16_t i = 0; i < listLen; i ++){
+    list[i]->timeToDeath = ts_readUint16(list[i]->data, 0) - (now - list[i]->arrivalTime);
+  }
+  // also... vertex arrivalTime should be uint32_t milliseconds of arrival... 
+  #warning not-yet sorted... 
+}
+
+// this handles internal transport... checking for errors along paths, and running flowcontrol 
+// returns true to wipe current item, false to leave-in-wait, 
+boolean internalTransport(stackItem* item, uint16_t ptr){
+  // we walk thru our little internal tree here, 
+  Vertex* vt = item->vt;
+  // ptr for the walk, use item->data[ptr] == PK_INSTRUCTION, not PK_PTR, 
+  uint16_t fwdPtr = ptr + 1;
+  // count # of ops, 
+  uint8_t opCount = 0;
+  // for a max. of 16 fwd steps, 
+  for(uint8_t s = 0; s < 16; s ++){
+    uint16_t arg = readArg(item->data, fwdPtr);
+    switch(PK_READKEY(item->data[fwdPtr])){
+      // ---------------------------------------- Internal Dir Cases 
+      case PK_SIB:
+        // check validity of route & shift our reference vt,
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during sib transport"); return true;
+        } else if (arg >= vt->parent->numChildren){
+          OSAP::error("no sibling " + String(arg) + " at " + vt->name + " during sib transport"); return true;
+        } else {
+          // this is it: we go fwds to this vt & end-of-switch statements increment ptrs
+          vt = vt->parent->children[arg];
+        }
+        break;
+      case PK_PARENT:
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during parent transport"); return true;
+        } else {
+          // likewise... 
+          vt = vt->parent;
+        }
+        break;
+      case PK_CHILD:
+        if(arg >= vt->numChildren){
+          OSAP::error("no child " + String(arg) + " at " + vt->name + " during child transport"); return true;
+        } else {
+          // again, just walk fwds... 
+          vt = vt->children[arg];
+        }
+        break;
+      // ---------------------------------------- Terminal / Exit Cases 
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD: 
+      case PK_DEST:
+      case PK_PINGREQ:
+      case PK_SCOPEREQ:
+      case PK_LLESCAPE:
+        // check / transport...
+        if(stackEmptySlot(vt, VT_STACK_DESTINATION)){
+          // walk the ptr fwds, 
+          walkPtr(item->data, item->vt, opCount, ptr);
+          // ingest at the new place, 
+          stackLoadSlot(vt, VT_STACK_DESTINATION, item->data, item->len);
+          // return true to clear it out, 
+          return true;
+        } else {
+          return false; 
+        }
+      default:
+        OSAP::error("internal transport failure, ptr walk ends at unknown key");
+        return true;
+    } // end switch 
+    fwdPtr += 2;
+    opCount ++;
+  } // end max-16-steps, 
+  // if we're past all 16 and didn't hit a terminal, pckt is eggregiously long, rm it 
+  return true;
+}
+
+// -------------------------------------------------------- LOOP Begins Here 
+
+// ... would be breadth-first, ideally 
+void osapLoop(Vertex* root){
+  // we want to build a list of items, recursing through... 
+  itemListLen = 0;
+  listSetupRecursor(root);
+  // check now if items are nearly oversized...
+  // see notes in the log from 2022-06-22 if this error occurs, 
+  if(itemListLen >= MAX_ITEMS_PER_LOOP - 2){
+    OSAP::error("loop items exceeds " + String(MAX_ITEMS_PER_LOOP) + ", breaking per-loop transport properties... pls fix", HALTING);
+  }
+  // stash high-water mark,
+  if(itemListLen > OSAP::loopItemsHighWaterMark) OSAP::loopItemsHighWaterMark = itemListLen;
+  // log 'em 
+  // OSAP::debug("list has " + String(itemListLen) + " elements", LOOP);
+  // otherwise we can carry on... the item should be sorted, global vars, 
+  listSort(itemList, itemListLen);
+  // then we can handle 'em one by one 
+  for(uint16_t i = 0; i < itemListLen; i ++){
+    osapItemHandler(itemList[i]);
+  }
+}
+
+void osapItemHandler(stackItem* item){
+  // clear dead items, 
+  if(item->timeToDeath < 0){
+    OSAP::debug(  "item at " + item->vt->name + " times out w/ " + String(item->timeToDeath) + 
+                  " ms to live, of " + String(ts_readUint16(item->data, 0)) + " ttl", LOOP);
+    stackClearSlot(item);
+    return;
+  }
+  // get a ptr for the item, 
+  uint16_t ptr = 0;
+  if(!findPtr(item->data, &ptr)){    
+    OSAP::error("item at " + item->vt->name + " unable to find ptr, deleting...");
+    stackClearSlot(item);
+    return;
+  }
+  // now the handle-switch, item->data[ptr] = PK_PTR, we switch on instruction which is behind that, 
+  switch(PK_READKEY(item->data[ptr + 1])){
+    // ------------------------------------------ Terminal / Destination Switches 
+    case PK_DEST:
+      item->vt->destHandler(item, ptr);
+      break;
+    case PK_PINGREQ:
+      item->vt->pingRequestHandler(item, ptr);
+      break;
+    case PK_SCOPEREQ:
+      item->vt->scopeRequestHandler(item, ptr);
+      break;
+    case PK_PINGRES:
+    case PK_SCOPERES:
+      OSAP::error("ping or scope request issued to " + item->vt->name + " not handling those in embedded", MEDIUM);
+      stackClearSlot(item);
+      break;
+    // ------------------------------------------ Internal Transport 
+    case PK_SIB:
+    case PK_PARENT:
+    case PK_CHILD:  // transport handler returns true if msg should be wiped, false if it should be cycled
+      if(internalTransport(item, ptr)){
+        stackClearSlot(item);
+      }
+      break;
+    // ------------------------------------------ Network Transport 
+    case PK_PFWD:
+      // port forward...
+      if(item->vt->vport == nullptr){
+        OSAP::error("pfwd to non-vport " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        if(item->vt->vport->cts()){
+          // walk one step, but only if fn returns true (having success) 
+          if(walkPtr(item->data, item->vt, 1, ptr)) item->vt->vport->send(item->data, item->len);
+          stackClearSlot(item);
+        } else {
+          // failed to send this turn (flow controlled), will return here next round 
+        }
+      }
+      break;
+    case PK_BFWD:
+    case PK_BBRD:
+      // bus forward / bus broadcast: 
+      if(item->vt->vbus == nullptr){
+        OSAP::error("bfwd to non-vbus " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        // arg is rxAddr for bus-forwards, is broadcastChannel for bus-broadcast, 
+        uint16_t arg = readArg(item->data, ptr + 1);
+        if(item->data[ptr + 1] == PK_BFWD){
+          if(item->vt->vbus->cts(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              item->vt->vbus->send(item->data, item->len, arg);
+            } else {
+              OSAP::error("bfwd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bfwd (flow controlled), returning here next round... 
+          }
+        } else if (item->data[ptr + 1] == PK_BBRD){
+          if(item->vt->vbus->ctb(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              // OSAP::debug("broadcasting on ch " + String(arg));
+              item->vt->vbus->broadcast(item->data, item->len, arg);
+            } else {
+              OSAP::error("bbrd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bbrd, returning next... 
+          }
+        } else {
+          // doesn't make any sense, we switched in on these terms... 
+          OSAP::error("absolute nonsense", MEDIUM);
+          stackClearSlot(item);
+        }
+      }
+      break;
+    case PK_LLESCAPE:
+      OSAP::error("lldebug to embedded, dumping", MINOR);
+      stackClearSlot(item);
+      break;
+    default:
+      OSAP::error("unrecognized ptr to " + item->vt->name + " " + String(PK_READKEY(item->data[ptr + 1])), MINOR);
+      stackClearSlot(item);
+      // error, delete, 
+      break;
+  } // end swiiiitch 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/loop.h b/system/firmware/lpf-loadcell-amp/src/osape/core/loop.h
new file mode 100644
index 0000000000000000000000000000000000000000..5022aa16c00da6b40864ca8f09432dab0744ad04
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/loop.h
@@ -0,0 +1,25 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef LOOP_H_
+#define LOOP_H_ 
+
+#include "vertex.h"
+
+// we loop, 
+void osapLoop(Vertex* root);
+// we handle, 
+void osapItemHandler(stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/osap.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/osap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acde43271ecb27ea482e1b1079b02d847a15fed9
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/osap.cpp
@@ -0,0 +1,111 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "loop.h"
+#include "packets.h"
+#include "../utils/cobs.h"
+
+// stash most recents, and counts, and high water mark, 
+uint32_t OSAP::loopItemsHighWaterMark = 0;
+uint32_t errorCount = 0;
+uint32_t debugCount = 0;
+// strings...
+unsigned char latestError[VT_SLOTSIZE];
+unsigned char latestDebug[VT_SLOTSIZE];
+uint16_t latestErrorLen = 0;
+uint16_t latestDebugLen = 0;
+
+OSAP::OSAP(String _name) : Vertex("rt_" + _name){};
+
+void OSAP::loop(void){
+  // this is the root, so we kick all of the internal net operation from here 
+  osapLoop(this);
+}
+
+void OSAP::destHandler(stackItem* item, uint16_t ptr){
+  // classic switch on 'em 
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == ROOT_KEY, ptr + 3 = ID (if ack req.) 
+  uint16_t wptr = 0;
+  uint16_t len = 0;
+  switch(item->data[ptr + 2]){
+    case RT_DBG_STAT:
+    case RT_DBG_ERRMSG:
+    case RT_DBG_DBGMSG:
+      // return w/ the res key & same issuing ID 
+      payload[wptr ++] = PK_DEST;
+      payload[wptr ++] = RT_DBG_RES;
+      payload[wptr ++] = item->data[ptr + 3];
+      // stash high water mark, errormsg count, debugmsgcount 
+      ts_writeUint32(OSAP::loopItemsHighWaterMark, payload, &wptr);
+      ts_writeUint32(errorCount, payload, &wptr);
+      ts_writeUint32(debugCount, payload, &wptr);
+      // optionally, a string... I know we switch() then if(), it's uggo, 
+      if(item->data[ptr + 2] == RT_DBG_ERRMSG){
+        ts_writeString(latestError, latestErrorLen, payload, &wptr, VT_SLOTSIZE / 2);
+      } else if (item->data[ptr + 2] == RT_DBG_DBGMSG){
+        ts_writeString(latestDebug, latestDebugLen, payload, &wptr, VT_SLOTSIZE / 2);
+      }
+      // that's the payload, I figure, 
+      len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+      stackClearSlot(item);
+      stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      break;
+    default:
+      OSAP::error("unrecognized key to root node " + String(item->data[ptr + 2]));
+      stackClearSlot(item);
+      break;
+  }
+}
+
+uint8_t errBuf[255];
+uint8_t errBufEncoded[255];
+
+void debugPrint(String msg){
+  // whatever you want,
+  uint32_t len = msg.length();
+  // max this long, per the serlink bounds 
+  if(len + 9 > 255) len = 255 - 9;
+  // header... 
+  errBuf[0] = len + 8;  // len, key, cobs start + end, strlen (4) 
+  errBuf[1] = 172;      // serialLink debug key 
+  errBuf[2] = len & 255;
+  errBuf[3] = (len >> 8) & 255;
+  errBuf[4] = (len >> 16) & 255;
+  errBuf[5] = (len >> 24) & 255;
+  msg.getBytes(&(errBuf[6]), len + 1);
+  // encode from 2, leaving the len, key header... 
+  size_t ecl = cobsEncode(&(errBuf[2]), len + 4, errBufEncoded);
+  // what in god blazes ? copy back from encoded -> previous... 
+  memcpy(&(errBuf[2]), errBufEncoded, ecl);
+  // set tail to zero, to delineate, 
+  errBuf[errBuf[0] - 1] = 0;
+  // direct escape 
+  Serial.write(errBuf, errBuf[0]);
+}
+
+void OSAP::error(String msg, OSAPErrorLevels lvl){
+  //const char* str = msg.c_str();
+  msg.getBytes(latestError, VT_SLOTSIZE);
+  latestErrorLen = msg.length();
+  errorCount ++;
+  debugPrint(msg);
+}
+
+void OSAP::debug(String msg, OSAPDebugStreams stream){
+  msg.getBytes(latestDebug, VT_SLOTSIZE);
+  latestDebugLen = msg.length();
+  debugCount ++;
+  debugPrint(msg);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/osap.h b/system/firmware/lpf-loadcell-amp/src/osape/core/osap.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c2c9d789ebd23ba452c7259c3423088ff2b9f
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/osap.h
@@ -0,0 +1,38 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+
+// largely semantic class, OSAP represents the root vertex in whichever context 
+// and it's where run the main loop from, etc... 
+// here is where we coordinate context-level stuff: adding new instances, 
+// stashing error messages & counts, etc, 
+
+enum OSAPErrorLevels { HALTING, MEDIUM, MINOR };
+enum OSAPDebugStreams { DEFAULT, LOOP };
+
+class OSAP : public Vertex {
+  public: 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr);
+    OSAP(String _name);// : Vertex(_name);
+    static void error(String msg, OSAPErrorLevels lvl = MINOR );
+    static void debug(String msg, OSAPDebugStreams stream = DEFAULT );
+    static uint32_t loopItemsHighWaterMark;
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/packets.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/packets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf83928d99d3c173d0efdef40ab614dc2433b409
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/packets.cpp
@@ -0,0 +1,193 @@
+/*
+osap/packets.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "packets.h"
+#include "ts.h"
+#include "osap.h"
+
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg){
+  buf[ptr] = key | (0b00001111 & (arg >> 8));
+  buf[ptr + 1] = arg & 0b11111111;
+}
+// not sure how I want to do this yet... 
+uint16_t readArg(uint8_t* buf, uint16_t ptr){
+  return ((buf[ptr] & 0b00001111) << 8) | buf[ptr + 1];
+}
+
+boolean findPtr(uint8_t* pck, uint16_t* pt){
+  // 1st instruction is always at pck[4], pck[0][1] == ttl, pck[2][3] == segSize 
+  uint16_t ptr = 4;
+  // there's a potential speedup where we assume given *pt is already incremented somewhat, 
+  // maybe shaves some ns... but here we just look fresh every time, 
+  for(uint8_t i = 0; i < 16; i ++){
+    switch(PK_READKEY(pck[ptr])){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        ptr += 2;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+boolean walkPtr(uint8_t* pck, Vertex* source, uint8_t steps, uint16_t ptr){
+  // if the ptr we were handed isn't in the right spot, try to find it... 
+  if(pck[ptr] != PK_PTR){
+    // if that fails, bail... 
+    if(!findPtr(pck, &ptr)){
+      OSAP::error("before a ptr walk, ptr is out of place...");
+      return false;
+    }
+  }
+  // carry on w/ the walking algo, 
+  for(uint8_t s = 0; s < steps; s ++){
+    switch PK_READKEY(pck[ptr + 1]){
+      case PK_SIB:
+        {
+          // stash indice from-whence it came,
+          uint16_t txIndice = source->indice;
+          // for loop's next step, this is the source now, 
+          source = source->parent->children[readArg(pck, ptr + 1)];
+          // where ptr is currently, we stash new key/pair for a reversal, 
+          writeKeyArgPair(pck, ptr, PK_SIB, txIndice);
+          // increment packet's ptr, and our own... 
+          pck[ptr + 2] = PK_PTR; 
+          ptr += 2;
+        }
+        break;
+      case PK_PARENT:
+        // reversal for a 'parent' instruction is to bounce back down to the child, 
+        writeKeyArgPair(pck, ptr, PK_CHILD, source->indice);
+        // next source is now...
+        source = source->parent;
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      case PK_CHILD:
+        // next source is... 
+        source = source->children[readArg(pck, ptr + 1)];
+        // reversal for 'child' instruction is to go back up to parent, 
+        writeKeyArgPair(pck, ptr, PK_PARENT, 0);
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2; 
+        break;
+      case PK_PFWD:
+        // reversal for pfwd instruction is identical, 
+        writeKeyArgPair(pck, ptr, PK_PFWD, 0);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // though this should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have port fwd w/ more than one step");
+          return false;
+        }
+        break;
+      case PK_BFWD:
+        // reversal for bfwd instruction is to return *up*... 
+        writeKeyArgPair(pck, ptr, PK_BFWD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // this also should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have bus fwd w/ more than one step");
+          return false; 
+        }
+        break;
+      case PK_BBRD:
+        // broadcasts are a little strange, we also stuff the ownRxAddr in,
+        writeKeyArgPair(pck, ptr, PK_BBRD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      default:
+        OSAP::error("have out of place keys in the ptr walk...");
+        return false;
+    }
+  } // end steps, alleged success,  
+  return true; 
+}
+
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen){
+  uint16_t wptr = 0;
+  ts_writeUint16(route->ttl, gram, &wptr);
+  ts_writeUint16(route->segSize, gram, &wptr);
+  memcpy(&(gram[wptr]), route->path, route->pathLen);
+  wptr += route->pathLen;
+  if(wptr + payloadLen > route->segSize){
+    OSAP::error("writeDatagram asked to write packet that exceeds segSize, bailing", MEDIUM);
+    return 0;
+  }
+  memcpy(&(gram[wptr]), payload, payloadLen);
+  wptr += payloadLen;
+  return wptr;
+}
+
+// original gram, payload, len, 
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen){
+  // 1st up, we can straight copy the 1st 4 bytes, 
+  memcpy(gram, ogGram, 4);
+  // now find a ptr, 
+  uint16_t ptr = 0;
+  if(!findPtr(ogGram, &ptr)){
+    OSAP::error("writeReply can't find the pointer...", MEDIUM);
+    return 0;
+  }
+  // do we have enough space? it's the minimum of the allowed segsize & stated maxGramLength, 
+  maxGramLength = min(maxGramLength, ts_readUint16(ogGram, 2));
+  if(ptr + 1 + payloadLen > maxGramLength){
+    OSAP::error("writeReply asked to write packet that exceeds maxGramLength, bailing", MEDIUM);
+    return 0;
+  }
+  // write the payload in, apres-pointer, 
+  memcpy(&(gram[ptr + 1]), payload, payloadLen);
+  // now we can do a little reversing... 
+  uint16_t wptr = 4;
+  uint16_t end = ptr;
+  uint16_t rptr = ptr;
+  // 1st byte... the ptr, 
+  gram[wptr ++] = PK_PTR;
+  // now for a max 16 steps, 
+  for(uint8_t h = 0; h < 16; h ++){
+    if(wptr >= end) break;
+    rptr -= 2;
+    switch(PK_READKEY(ogGram[rptr])){
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        gram[wptr ++] = ogGram[rptr];
+        gram[wptr ++] = ogGram[rptr + 1];
+        break;
+      default:
+        OSAP::error("writeReply fails to reverse this packet, bailing", MEDIUM);
+        return 0;
+    }
+  } // end thru-loop, 
+  // it's written, return the len  // we had gram[ptr] = PK_PTR, so len was ptr + 1, then added payloadLen, 
+  return end + 1 + payloadLen;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/packets.h b/system/firmware/lpf-loadcell-amp/src/osape/core/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..914656be1eb7656f481915438a10701edad23280
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/packets.h
@@ -0,0 +1,48 @@
+/*
+osap/packets.h
+
+reading / writing from osap packets / datagrams 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_PACKETS_H_
+#define OSAP_PACKETS_H_
+
+#include <Arduino.h>
+#include "vertex.h"
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 240
+#define PK_DEST 224
+#define PK_PINGREQ 192 
+#define PK_PINGRES 176 
+#define PK_SCOPEREQ 160 
+#define PK_SCOPERES 144 
+#define PK_SIB 16 
+#define PK_PARENT 32 
+#define PK_CHILD 48 
+#define PK_PFWD 64 
+#define PK_BFWD 80
+#define PK_BBRD 96 
+#define PK_LLESCAPE 112 
+
+// to read *just the key* from key, arg pair
+#define PK_READKEY(data) (data & 0b11110000)
+
+// packet utes, 
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg);
+uint16_t readArg(uint8_t* buf, uint16_t ptr);
+boolean findPtr(uint8_t* pck, uint16_t* ptr);
+boolean walkPtr(uint8_t* pck, Vertex* vt, uint8_t steps, uint16_t ptr = 4);
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen);
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/routes.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/routes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6caea0c0a00c56f339f2fdb7ec4b02278e1faf73
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/routes.cpp
@@ -0,0 +1,55 @@
+/*
+osap/routes.cpp
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "routes.h"
+#include "packets.h"
+
+Route::Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize){
+  ttl = _ttl;
+  segSize = _segSize;
+  // nope, 
+  if(_pathLen > 64){
+    _pathLen = 0;
+  }
+  memcpy(path, _path, _pathLen);
+  pathLen = _pathLen;
+}
+
+Route::Route(void){
+  path[pathLen ++] = PK_PTR;
+}
+
+Route* Route::sib(uint16_t indice){
+  writeKeyArgPair(path, pathLen, PK_SIB, indice);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::pfwd(void){
+  writeKeyArgPair(path, pathLen, PK_PFWD, 0);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bfwd(uint16_t rxAddr){
+  writeKeyArgPair(path, pathLen, PK_BFWD, rxAddr);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bbrd(uint16_t channel){
+  writeKeyArgPair(path, pathLen, PK_BBRD, channel);
+  pathLen += 2;
+  return this; 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/routes.h b/system/firmware/lpf-loadcell-amp/src/osape/core/routes.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2bb3c97cffb7df24867de4efe7489b40daa4a0e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/routes.h
@@ -0,0 +1,38 @@
+/*
+osap/routes.h
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_ROUTES_H_
+#define OSAP_ROUTES_H_
+
+#include <Arduino.h>
+
+// a route type... 
+class Route {
+  public:
+    uint8_t path[64];
+    uint16_t pathLen = 0;
+    uint16_t ttl = 1000;
+    uint16_t segSize = 128;
+    // write-direct constructor, 
+    Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize);
+    // write-along constructor, 
+    Route(void);
+    // pass-thru initialize constructors, 
+    Route* sib(uint16_t indice);
+    Route* pfwd(void);
+    Route* bfwd(uint16_t rxAddr);
+    Route* bbrd(uint16_t channel);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/stack.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/stack.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..401bd7103f872bd141172d945ff2b2a8cb93e36f
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/stack.cpp
@@ -0,0 +1,138 @@
+/*
+osap/stack.cpp
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "stack.h"
+#include "vertex.h"
+#include "osap.h"
+
+// ---------------------------------------------- Stack Tools 
+
+void stackReset(Vertex* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+      // and ptrs to self, 
+      vt->stack[od][s].vt = vt;
+      vt->stack[od][s].od = od;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(Vertex* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //DEBUG("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item){
+  // this would be deadly, so:
+  if(od > 1) {
+    OSAP::error("stackClearSlot, od > 1, badness", MEDIUM);
+    return;
+  }
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+  }
+  // now we callback to the vertex; these fns are often used to clear flowcontrol condns 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      vt->onDestinationStackClear(indice);
+      break;
+    default:  // guarded against this above... 
+      break;
+  }
+}
+
+void stackClearSlot(stackItem* item){
+  stackClearSlot(item->vt, item->od, item);
+}
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/stack.h b/system/firmware/lpf-loadcell-amp/src/osape/core/stack.h
new file mode 100644
index 0000000000000000000000000000000000000000..79151239b987f025150dc6f1ac580cfc4e474887
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/stack.h
@@ -0,0 +1,54 @@
+/*
+osap/stack.h
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STACK_H_
+#define STACK_H_ 
+
+#include <Arduino.h>
+#include "./osap_config.h" 
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+class Vertex;
+
+// core routing layer chunk-of-stuff, 
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+typedef struct stackItem {
+  uint8_t data[VT_SLOTSIZE];          // data bytes
+  uint16_t len = 0;                   // data bytes count 
+  uint32_t arrivalTime = 0;           // ms-since-system-alive, time at last ingest
+  int32_t timeToDeath = 0;            // ms of time until pckt vanishes on this hop
+  Vertex* vt;                         // vertex to whomst we belong, 
+  uint8_t od;                         // origin / destination to which we belong, 
+  uint8_t indice;                     // actual physical position in the stack 
+  uint16_t ptr = 0;                   // current data[ptr] == 88 
+  stackItem* next = nullptr;          // linked ringbuffer next 
+  stackItem* previous = nullptr;      // linked ringbuffer previous 
+} stackItem;
+
+// stack setup / reset 
+void stackReset(Vertex* vt);
+
+// stack origin side 
+boolean stackEmptySlot(Vertex* vt, uint8_t od);
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// stack exit side 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item);
+void stackClearSlot(stackItem* item);
+
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/ts.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/ts.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cd3fdc9c1c249d25b22b75fa9fc69f311d04c19
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/ts.cpp
@@ -0,0 +1,183 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+// ---------------------------------------------- Reading 
+
+// boolean 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr){
+  if(buf[(*ptr) ++]){
+    *val = true;
+  } else {
+    *val = false;
+  }
+}
+
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr){
+  boolean val = buf[(*ptr)] ? true : false;
+  (*ptr) += 1;
+  return val;
+}
+
+// uint8 
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr){
+  uint8_t val = buf[(*ptr)];
+  (*ptr) += 1;
+  return val;
+}
+
+// uint16 
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+#warning some of these are pretty vague, i.e. this ingests a pointer *not as a pointer* (lol)
+// so it doesn't increment it, whereas the readUint8 above *does so* - ... ?? pick a style ? 
+uint16_t ts_readUint16(unsigned char* buf, uint16_t ptr){
+  return (buf[ptr + 1] << 8) | buf[ptr];
+}
+
+// uint32 
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr){
+  uint32_t val = (buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)]);
+  (*ptr) += 4;
+  return val;
+}
+
+// int32 
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.i;
+}
+
+// float32 
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.f;
+}
+
+// -------------------------------------------------------- Writing 
+
+// boolean
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+// unsigned 
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+// signed 
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int16 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+}
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+// floats 
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+// string, overloaded ?
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen){
+  if(strLen > maxLen) strLen = maxLen;
+  buf[(*ptr) ++] = strLen & 255;
+  buf[(*ptr) ++] = (strLen >> 8) & 255;
+  buf[(*ptr) ++] = (strLen >> 16) & 255;
+  buf[(*ptr) ++] = (strLen >> 24) & 255;
+  // write in one-by-one, surely there is a better way, 
+  for(uint16_t i = 0; i < strLen; i ++){
+    buf[(*ptr) ++] = str[i];
+  }
+  *ptr += strLen;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/ts.h b/system/firmware/lpf-loadcell-amp/src/osape/core/ts.h
new file mode 100644
index 0000000000000000000000000000000000000000..63e77b2b02c0f7716bb1cba55cc9eef613d1207f
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/ts.h
@@ -0,0 +1,157 @@
+/*
+core/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22       // top level 
+#define VT_TYPE_MODULE 23     // collection of things (?) or something, idk yet 
+#define VT_TYPE_ENDPOINT 24   // software endpoint w/ read/write semantics 
+#define VT_TYPE_QUERY 25 
+#define VT_TYPE_ENDPOINT_MULTISEG 26 // likewise, but requring multisegment transmission 
+#define VT_TYPE_CODE 25       // autonomous graph dwellers 
+#define VT_TYPE_VPORT 44      // virtual ports 
+#define VT_TYPE_VBUS 45       // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+#define EP_ROUTE_QUERY_REQ 141 
+#define EP_ROUTE_QUERY_RES 142
+#define EP_ROUTE_SET_REQ 143
+#define EP_ROUTE_SET_RES 144 
+#define EP_ROUTE_RM_REQ 147
+#define EP_ROUTE_RM_RES 148 
+
+#define EP_ROUTEMODE_ACKED 167
+#define EP_ROUTEMODE_ACKLESS 168 
+
+// -------------------------------------------------------- Root Keys 
+
+#define RT_DBG_STAT 151
+#define RT_DBG_ERRMSG 152 
+#define RT_DBG_DBGMSG 153
+#define RT_DBG_RES 161
+
+// -------------------------------------------------------- VBus MVC Keys 
+
+#define VBUS_BROADCAST_MAP_REQ 145
+#define VBUS_BROADCAST_MAP_RES 146
+#define VBUS_BROADCAST_QUERY_REQ 141
+#define VBUS_BROADCAST_QUERY_RES 142
+#define VBUS_BROADCAST_SET_REQ 143
+#define VBUS_BROADCAST_SET_RES 144 
+#define VBUS_BROADCAST_RM_REQ 147 
+#define VBUS_BROADCAST_RM_RES 148 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int16 {
+  uint8_t bytes[2];
+  int16_t i;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr);
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr);
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+uint16_t ts_readUint16(uint8_t* buf, uint16_t ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr);
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+// -------------------------------------------------------- Writing 
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.cpp b/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9ce012af681a42b059c6585888d1db806dd2ab51
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.cpp
@@ -0,0 +1,327 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "stack.h"
+#include "osap.h"
+#include "packets.h"
+
+// ---------------------------------------------- Temporary Stash 
+
+uint8_t Vertex::payload[VT_SLOTSIZE];
+uint8_t Vertex::datagram[VT_SLOTSIZE];
+
+// ---------------------------------------------- Vertex Constructor and Defaults 
+
+Vertex::Vertex( 
+  Vertex* _parent, String _name, 
+  void (*_loop)(Vertex* vt),
+  void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+  void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+){
+  // name self, reset stack... 
+  name = _name;
+  stackReset(this);
+  // callback assignments... 
+  loop_cb = _loop;
+  onOriginStackClear_cb = _onOriginStackClear;
+  onDestinationStackClear_cb = _onDestinationStackClear;
+  // insert self to osap net,
+  if(_parent == nullptr){
+    type = VT_TYPE_ROOT;
+    indice = 0;
+  } else {
+    if (_parent->numChildren >= VT_MAXCHILDREN) {
+      OSAP::error("trying to nest a vertex under " + _parent->name + " but we have reached VT_MAXCHILDREN limit", HALTING);
+    } else {
+      this->indice = _parent->numChildren;
+      this->parent = _parent;
+      _parent->children[_parent->numChildren ++] = this;
+    }
+  }
+}
+
+void Vertex::loop(void){
+  if(loop_cb != nullptr) return loop_cb(this);
+}
+
+void Vertex::destHandler(stackItem* item, uint16_t ptr){
+  // generic handler...
+  OSAP::debug("generic destHandler at " + name);
+  stackClearSlot(item);
+}
+
+void Vertex::pingRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_PINGRES;
+  payload[1] = item->data[ptr + 2];
+  // write a new gram, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 2);
+  // clear previous, 
+  stackClearSlot(item);
+  // load next... there will be one empty, as this has just arrived here... & we just wiped it 
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+void Vertex::scopeRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_SCOPERES;
+  payload[1] = item->data[ptr + 2];
+  // next items write starting here, 
+  uint16_t wptr = 2;
+  // scope time-tag, 
+  ts_writeUint32(scopeTimeTag, payload, &wptr);
+  // and read in the previous scope (this is traversal state required to delineate loops in the graph) 
+  uint16_t rptr = ptr + 3;
+  ts_readUint32(&scopeTimeTag, item->data, &rptr);
+  // write the vertex type,  
+  payload[wptr ++] = type;
+  // vport / vbus link states, 
+  if(type == VT_TYPE_VPORT){
+    payload[wptr ++] = (vport->isOpen() ? 1 : 0);
+  } else if (type == VT_TYPE_VBUS){
+    uint16_t addrSize = vbus->addrSpaceSize;
+    uint16_t addr = 0;
+    // ok we write the address size in first, then our own rxaddr, 
+    ts_writeUint16(vbus->addrSpaceSize, payload, &wptr);
+    ts_writeUint16(vbus->ownRxAddr, payload, &wptr);
+    // then *so long a we're not overwriting*, we stuff link-state bytes, 
+    while(wptr + 8 + name.length() <= VT_SLOTSIZE){
+      payload[wptr] = 0;
+      for(uint8_t b = 0; b < 8; b ++){
+        payload[wptr] |= (vbus->isOpen(addr) ? 1 : 0) << b;
+        addr ++;
+        if(addr >= addrSize) goto end;
+      }
+      wptr ++;
+    }
+    end:
+    wptr ++; // += 1 more, so we write into next, 
+  }
+  // our own indice, # siblings, and # children, 
+  ts_writeUint16(indice, payload, &wptr);
+  if(parent != nullptr){
+    ts_writeUint16(parent->numChildren, payload, &wptr);
+  } else {
+    ts_writeUint16(0, payload, &wptr);
+  }
+  ts_writeUint16(numChildren, payload, &wptr);
+  // finally, our string name:
+  ts_writeString(name, payload, &wptr);
+  // and roll that back up, rm old, and ship it, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+  stackClearSlot(item);
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+
+void Vertex::onOriginStackClear(uint8_t slot){
+  if(onOriginStackClear_cb != nullptr) return onOriginStackClear_cb(this, slot);
+}
+
+void Vertex::onDestinationStackClear(uint8_t slot){
+  if(onDestinationStackClear_cb != nullptr) return onDestinationStackClear_cb(this, slot);
+}
+
+// ---------------------------------------------- VPort Constructor and Defaults 
+
+VPort::VPort(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vp_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VPORT;
+  vport = this; 
+}
+
+// ---------------------------------------------- VBus Constructor and Defaults 
+
+VBus::VBus(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vb_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VBUS;
+  vbus = this;
+  // these should all init to nullptr, 
+  for(uint8_t ch = 0; ch < VBUS_MAX_BROADCAST_CHANNELS; ch ++){
+    broadcastChannels[ch] = nullptr;
+  }
+}
+
+void VBus::injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  // ok so first we want to see if we have anything sub'd to this channel, so
+  if(broadcastChannels[broadcastChannel] != nullptr){
+    // we have a route, so we want to load this data *as we inject some new path segments* 
+    Route* route = broadcastChannels[broadcastChannel];
+    // we could definitely do this faster w/o using the stackLoadSlot fn, but we won't do that yet... 
+    // will use the vertex-global datagram stash for that 
+    uint16_t ptr = 0; 
+    if(!findPtr(data, &ptr)){ OSAP::error("can't find ptr during broadcast injest", MEDIUM); return; }
+    // packet should look like 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <payload>
+    // we want to inject the channel's route such that 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <ch_route>, <payload>
+    // shouldn't actually be too difficult, eh?
+    // we do need to guard on lengths, 
+    if(len + route->pathLen > VT_SLOTSIZE){ OSAP::error("datagram + channel route is too large", MEDIUM); return; }
+    // copy up to PTR: pck[ptr] == PK_PTR, so we want to *include* this byte, having len ptr + 1, 
+    memcpy(datagram, data, ptr + 1);
+    // copy in route, but recall that as initialized, route->path[0] == PK_PTR, we don't want to double that up, 
+    memcpy(&(datagram[ptr + 1]), &(route->path[1]), route->pathLen - 1);
+    // then the rest of the gram, from just after-the-ptr, to end, 
+    memcpy(&datagram[ptr + 1 + route->pathLen - 1], &(data[ptr + 1]), len - ptr - 1);
+    // now we can load this in, 
+    stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len + route->pathLen - 1);
+    // aye that's it innit? 
+  }
+}
+
+void VBus::setBroadcastChannel(uint8_t channel, Route* route){
+  if(channel >= VBUS_MAX_BROADCAST_CHANNELS) return;
+  // seems a little sus, idk 
+  broadcastChannels[channel] = route;
+}
+
+void VBus::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == the key we're switching on...
+  switch(item->data[ptr + 2]){
+    case VBUS_BROADCAST_MAP_REQ:
+      // mvc request a map of our active broadcast channels, this is akin to bus link-state-scope packet
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_MAP_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // max length of channels... max 255, same as max endpoint routes (?) 
+        // this is maybe an error, consult packet spec (transport layer) for completeness, 
+        // time being... rare to have > 255 broadcast channels, 
+        payload[wptr ++] = VBUS_MAX_BROADCAST_CHANNELS;
+        // then *so long a we're not overwriting*, we stuff link-state bytes, 
+        // idk, 32 is arbitrary, we have to account for return-route length properly... 
+        uint16_t channel = 0;
+        while(wptr + 32 <= VT_SLOTSIZE){
+          payload[wptr] = 0;
+          for(uint8_t b = 0; b < 8; b ++){
+            payload[wptr] |= (broadcastChannels[channel] == nullptr ? 0 : 1) << b;
+            channel ++;
+            if(channel >= VBUS_MAX_BROADCAST_CHANNELS) goto end;
+          }
+          wptr ++;
+        }
+        end:
+        wptr ++; // += 1 more, so we write into next, 
+        // we're ready to write the reply back, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_QUERY_REQ:
+      // mvc requests broadcast channel info on a particular channel, 
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_QUERY_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // the indice of the channel we're looking at, 
+        uint16_t ch = item->data[ptr + 4];
+        // if the ch exists, 
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS && broadcastChannels[ch] != nullptr){
+          payload[wptr ++] = 1;
+          // now... these are route objects, but we only use the path part... 
+          // but we'll re-use route-object serialization schemes from EP_ROUTE_QUERY_REQ 
+          ts_writeUint16(broadcastChannels[ch]->ttl, payload, &wptr);
+          ts_writeUint16(broadcastChannels[ch]->segSize, payload, &wptr);
+          // path copy 
+          memcpy(&(payload[wptr]), broadcastChannels[ch]->path, broadcastChannels[ch]->pathLen);
+          wptr += broadcastChannels[ch]->pathLen;
+        } else {
+          payload[wptr ++] = 0;
+        }
+        // write reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_SET_REQ:
+      // mvc requests to set a broadcast channel route 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // ch to write into...
+        uint8_t ch = item->data[ptr + 4];
+        // reply-write-pointer 
+        uint16_t wptr = 0;
+        // prep a response, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_SET_RES;
+        payload[wptr ++] = id;
+        if(ch >= VBUS_MAX_BROADCAST_CHANNELS){
+          // won't go 
+          OSAP::error("attempt to write to oob broadcast channel");
+          payload[wptr ++] = 0;
+        } else {
+          // should go 
+          payload[wptr ++] = 1;          
+          if(broadcastChannels[ch] != nullptr) OSAP::debug("overwriting previous broadcast ch at " + String(ch));
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          setBroadcastChannel(ch, new Route(path, pathLen, ttl, segSize));
+        }
+        // in any case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_RM_REQ:
+      // mvc requests to rm a broadcast channel, 
+      // todo / cleanliness: might be salient to 'write 0' to delete (?) who knows 
+      {
+        // id & indice to rm 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t ch = item->data[ptr + 4];
+        uint16_t wptr = 0;
+        // prep res 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_RM_RES;
+        payload[wptr ++] = id;
+        // can we rm ?
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS){
+          if(broadcastChannels[ch] != nullptr) {
+            delete broadcastChannels[ch];
+            broadcastChannels[ch] = nullptr;
+            payload[wptr ++] = 1;
+          } else {
+            // didn't exist, so, a bad delete: 
+            payload[wptr ++] = 0;
+          }
+        } else {
+          // bad req, should throw errors... 
+          payload[wptr ++] = 0;
+        }
+        // can send now, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    default:
+      OSAP::error("vbus rx msg w/ unrecognized vbus key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.h b/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.h
new file mode 100644
index 0000000000000000000000000000000000000000..842d5733f64fa6661165c84e2193b2c0604892d1
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/core/vertex.h
@@ -0,0 +1,131 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "routes.h"
+#include "stack.h"
+// vertex config is build dependent, define in <folder-containing-osape>/osapConfig.h 
+#include "./osap_config.h" 
+
+// we have the vertex type, 
+// since it contains ptrs to others of its type, we fwd declare the type...
+class Vertex;
+// ... 
+typedef struct stackItem stackItem;
+typedef struct VPort VPort;
+typedef struct VBus VBus;
+
+// default vt fns 
+void vtLoopDefault(Vertex* vt);
+void vtOnOriginStackClearDefault(Vertex* vt, uint8_t slot);
+void vtOnDestinationStackClearDefault(Vertex* vt, uint8_t slot);
+
+// addressable node in the graph ! 
+class Vertex {
+  public:
+    // just temporary stashes, used all over the place to prep messages... 
+    static uint8_t payload[VT_SLOTSIZE];
+    static uint8_t datagram[VT_SLOTSIZE];
+    // -------------------------------- FN PTRS 
+    // these are *genuine function ptrs* not member functions, my dudes 
+    void (*loop_cb)(Vertex* vt) = nullptr;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    void (*onDestinationStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    // -------------------------------- Methods
+    virtual void loop(void);
+    virtual void destHandler(stackItem* item, uint16_t ptr);
+    void pingRequestHandler(stackItem* item, uint16_t ptr);
+    void scopeRequestHandler(stackItem* item, uint16_t ptr);
+    virtual void onOriginStackClear(uint8_t slot);
+    virtual void onDestinationStackClear(uint8_t slot);
+    // -------------------------------- DATA
+    // a type, a position, a name 
+    uint8_t type = VT_TYPE_CODE;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we were last scoped (need for graph traversals, final implementation tbd)
+    uint32_t scopeTimeTag = 0;
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    Vertex* parent = nullptr;
+    Vertex* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // sometimes a vertex is a vport, sometimes it is a vbus, 
+    VPort* vport;
+    VBus* vbus;
+    // -------------------------------- CONSTRUCTORS 
+    Vertex( 
+      Vertex* _parent, 
+      String _name, 
+      void (*_loop)(Vertex* vt),
+      void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+      void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+    );
+    Vertex(Vertex* _parent, String _name) : Vertex(_parent, _name, nullptr, nullptr, nullptr){};
+    Vertex(String _name) : Vertex(nullptr, _name, nullptr, nullptr, nullptr){};
+};
+
+// ---------------------------------------------- VPort 
+
+class VPort : public Vertex {
+  public:
+    // -------------------------------- OK these bbs are methods, 
+    virtual void send(uint8_t* data, uint16_t len) = 0;
+    virtual boolean cts(void) = 0;
+    virtual boolean isOpen(void) = 0;
+    // base constructor, 
+    VPort(Vertex* _parent, String _name);
+};
+
+// ---------------------------------------------- VBus 
+
+class VBus : public Vertex{
+  public:
+    // -------------------------------- Methods: these are purely virtual... 
+    virtual void send(uint8_t* data, uint16_t len, uint8_t rxAddr) = 0;
+    virtual void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) = 0;
+    // clear to send, clear to broadcast, 
+    virtual boolean cts(uint8_t rxAddr) = 0;
+    virtual boolean ctb(uint8_t broadcastChannel) = 0;
+    // link state per rx-addr,
+    virtual boolean isOpen(uint8_t rxAddr) = 0;
+    // handle things aimed at us, for mvc etc 
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // busses can read-in to broadcasts,
+    void injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel);
+    // we have also... broadcast channels... these are little route stubs & channel pairs, which we just straight up index, 
+    Route* broadcastChannels[VBUS_MAX_BROADCAST_CHANNELS];
+    // have to update those... 
+    void setBroadcastChannel(uint8_t channel, Route* route);
+    // has an rx addr, 
+    uint16_t ownRxAddr = 0;
+    // has a width-of-addr-space, 
+    uint16_t addrSpaceSize = 0;
+    // base constructor, children inherit... 
+    VBus(Vertex* _parent, String _name);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.cpp b/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81cc05bb3b38d85273a838a4b05df31bff2783a9
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.h b/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.h
new file mode 100644
index 0000000000000000000000000000000000000000..b47070ca26d021f113da680a6835df65712d4007
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.cpp b/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d9fe310be794e69ef9040e2ee33a26bcf986f9
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.cpp
@@ -0,0 +1,351 @@
+/*
+osape/vertices/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "../core/osap.h"
+#include "../core/packets.h"
+
+// -------------------------------------------------------- Constructors 
+
+// route constructor 
+EndpointRoute::EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+  if(_mode != EP_ROUTEMODE_ACKED && _mode != EP_ROUTEMODE_ACKLESS){
+    _mode = EP_ROUTEMODE_ACKLESS;
+  }
+  route = _route;
+  ackMode = _mode;
+  timeoutLength = _timeoutLength;
+}
+
+EndpointRoute::~EndpointRoute(void){
+  delete route;
+}
+
+// base constructor, 
+Endpoint::Endpoint(
+  Vertex* _parent, String _name, 
+  EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+  boolean (*_beforeQuery)(void)
+) : Vertex(_parent, "ep_" + _name) {
+  // type, 
+	type = VT_TYPE_ENDPOINT;
+  // set callbacks,
+  if(_onData) onData_cb = _onData;
+  if(_beforeQuery) beforeQuery_cb = _beforeQuery;
+}
+
+// -------------------------------------------------------- Dummies / Defaults 
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+// -------------------------------------------------------- Endpoint Route / Write API 
+
+void Endpoint::write(uint8_t* _data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(data, _data, len);
+  dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state == EP_TX_AWAITING_ACK){
+      routes[r]->state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      routes[r]->state = EP_TX_FRESH;
+    }
+  }
+}
+
+// add a route to an endpoint, returns indice where it's dropped, 
+uint8_t Endpoint::addRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+	// guard against more-than-allowed routes 
+	if(numRoutes >= ENDPOINT_MAX_ROUTES) {
+    OSAP::error("route add is oob", MEDIUM); 
+    return 0;
+	}
+  // build, stash, increment 
+  uint8_t indice = numRoutes;
+  routes[numRoutes ++] = new EndpointRoute(_route, _mode, _timeoutLength);
+  return indice; 
+}
+
+boolean Endpoint::clearToWrite(void){
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+// -------------------------------------------------------- Loop 
+
+void Endpoint::loop(void){
+  // ok we are doing a time-based dispatch... 
+  unsigned long now = millis();
+  EndpointRoute* routeTxList[ENDPOINT_MAX_ROUTES];
+  uint8_t numTxRoutes = 0;
+  // stack fresh routes, and also transition timeouts / etc, 
+  // we make & sort this list, but set it up round-robin, since many 
+  // cases will see the same TTL & same write-to time, meaning routes that 
+  // happen to be in low indices would chance on "higher priority" 
+  uint8_t r = lastRouteServiced;
+  for(uint8_t i = 0; i < numRoutes; i ++){
+    r ++; if(r >= numRoutes) r = 0;
+    switch(routes[r]->state){
+      case EP_TX_FRESH:
+        routeTxList[numTxRoutes ++] = routes[r];
+        break;
+      case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_FRESH;
+        }
+      default:
+        // noop for IDLE / otherwise...
+        break;
+    }
+  }
+  // now, would do a sort... they're all fresh at the same time, so lowest TTL would win,
+  // this one we would want to be stable, meaning original order is preserved in 
+  // otherwise identical cases, since we round-robin fairness as well as TTL / TTD  
+  #warning no sort algo yet, 
+  // serve 'em... these are all EP_TX_FRESH state, 
+  for(r = 0; r < numTxRoutes; r ++){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // make sure we'll have enough space...
+      if(dataLen + routeTxList[r]->route->pathLen + 3 >= VT_SLOTSIZE){
+        OSAP::error("attempting to write oversized datagram at " + name, MEDIUM);
+        routeTxList[r]->state = EP_TX_IDLE;
+        continue;
+      }
+      // write dest key, mode key, & id if acked, 
+      uint16_t wptr = 0;
+      payload[wptr ++] = PK_DEST;
+      if(routeTxList[r]->ackMode == EP_ROUTEMODE_ACKLESS){
+        payload[wptr ++] = EP_SS_ACKLESS;
+      } else {
+        payload[wptr ++] = EP_SS_ACKED;
+        payload[wptr ++] = nextAckID;
+        routeTxList[r]->ackId = nextAckID;
+        nextAckID ++;
+      } 
+      // write data into the payload, 
+      memcpy(&(payload[wptr]), data, dataLen);
+      wptr += dataLen;
+      // write the packet, 
+      uint16_t len = writeDatagram(datagram, VT_SLOTSIZE, routeTxList[r]->route, payload, wptr);
+      // tx time is now, and state is awaiting ack, 
+      routeTxList[r]->lastTxTime = now;
+      routeTxList[r]->state = EP_TX_AWAITING_ACK;
+      lastRouteServiced = r;
+      // ingest it...
+      stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len);
+    } else {
+      // stack has no more empty slots, bail from the loop, 
+      break;
+    }
+  } // end fresh-tx-awaiting state checks, 
+}
+
+// -------------------------------------------------------- Destination Handler  
+
+void Endpoint::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == EP_KEY, ptr + 3 = ID (if ack req.) 
+  switch(item->data[ptr + 2]){
+    case EP_SS_ACKLESS:
+      { // singlesegment transmit-to-us, w/o ack, 
+        uint8_t* rxData = &(item->data[ptr + 3]); uint16_t rxLen = item->len - (ptr + 4);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+        switch(resp){
+          case EP_ONDATA_WAIT:    // in a wait case, we no-op / escape, it comes back around 
+            item->arrivalTime = millis();
+            break;
+          case EP_ONDATA_ACCEPT:  // here we copy it in, but carry on to the reject term to delete og gram
+            memcpy(data, rxData, rxLen);
+            dataLen = rxLen;
+          case EP_ONDATA_REJECT:  // here we simply reject it, 
+            stackClearSlot(item);
+            break;
+        } // end resp-handler, 
+      }
+      break;
+    case EP_SS_ACKED:
+      { // singlesegment transmit-to-us, w/ ack, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t* rxData = &(item->data[ptr + 4]); uint16_t rxLen = item->len - (ptr + 5);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+          switch(resp){
+            case EP_ONDATA_WAIT: // this is a little danger-danger, 
+              item->arrivalTime = millis();
+              break;
+            case EP_ONDATA_ACCEPT:
+              memcpy(data, rxData, rxLen);
+              dataLen = rxLen;
+            case EP_ONDATA_REJECT:
+              // write the ack, ship it, 
+              payload[0] = PK_DEST;
+              payload[1] = EP_SS_ACK;
+              payload[2] = id;
+              uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 3);
+              stackClearSlot(item);
+              stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+              break;
+          }
+      }
+      break;
+    case EP_QUERY:
+      {
+        // beforeQuery, 
+        beforeQuery_cb();
+        // request for our data, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_QUERY_RESP;
+        payload[2] = item->data[ptr + 3];
+        memcpy(&(payload[3]), data, dataLen);
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, dataLen + 3);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_SS_ACK:
+      // acks to us, 
+      for(uint8_t r = 0; r < numRoutes; r ++){
+        if(item->data[ptr + 3] == routes[r]->ackId){
+          switch(routes[r]->state){
+            case EP_TX_AWAITING_ACK:
+              routes[r]->state = EP_TX_IDLE;
+              goto ackEnd;
+            case EP_TX_AWAITING_AND_FRESH:
+              routes[r]->state = EP_TX_FRESH;
+              goto ackEnd;
+            case EP_TX_FRESH:
+            case EP_TX_IDLE:
+            default:
+              // these are nonsense states, likely double-transmits, likely safely ignored,
+              goto ackEnd;
+          } // end switch 
+        }
+      } // end for-each route, if we've reached this point, still dump it;
+      ackEnd:
+      stackClearSlot(item);
+      break;
+    case EP_ROUTE_QUERY_REQ:
+      // MVC request for a route of ours, 
+      {
+        uint8_t id = item->data[ptr + 3];
+        uint16_t r = ts_readUint16(item->data, ptr + 4);
+        uint16_t wptr = 0;
+        // dest, key, id... mode, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_QUERY_RES;
+        payload[wptr ++] = id;
+        if(r < numRoutes){
+          payload[wptr ++] = routes[r]->ackMode;
+          // ttl, segsize, 
+          ts_writeUint16(routes[r]->route->ttl, payload, &wptr);
+          ts_writeUint16(routes[r]->route->segSize, payload, &wptr);
+          // path ! 
+          memcpy(&(payload[wptr]), routes[r]->route->path, routes[r]->route->pathLen);
+          wptr += routes[r]->route->pathLen;
+        } else {
+          payload[wptr ++] = 0; // no-route-here, 
+        }
+        // clear request, write reply in place, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_SET_REQ:
+      // MVC request to set a new route, 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_SET_RES;
+        payload[2] = id;
+        if(numRoutes + 1 <= ENDPOINT_MAX_ROUTES){
+          // tell call-er it should work, 
+          payload[3] = 1;
+          // gather & set route, 
+          uint8_t mode = item->data[ptr + 4];
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          OSAP::debug("adding path... w/ ttl " + String(ttl) + " ss " + String(segSize) + " pathLen " + String(pathLen));
+          uint8_t routeIndice = addRoute(new Route(path, pathLen, ttl, segSize), mode);
+          payload[4] = routeIndice;
+        } else {
+          // nope, 
+          payload[3] = 0;
+          payload[4] = 0;
+        }
+        // either case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 5);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_RM_REQ:
+      // MVC request to rm a route... 
+      {
+        // msg id, & indice to remove, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t r = item->data[ptr + 4];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_RM_RES;
+        payload[2] = id;
+        if(r < numRoutes){
+          // RM ok, 
+          payload[3] = 1;
+          // delete / run destructor 
+          delete routes[r];
+          // shift...
+          for(uint8_t i = r; i < numRoutes - 1; i ++){
+            routes[i] = routes[i + 1];
+          }
+          // last is null, 
+          routes[numRoutes] = nullptr;
+          numRoutes --;
+        } else {
+          // rm not-ok
+          payload[3] = 0;
+        }
+        // either case, write reply 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 4);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    default:
+      OSAP::error("endpoint rx msg w/ unrecognized endpoint key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } // end switch... 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.h b/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.h
new file mode 100644
index 0000000000000000000000000000000000000000..b14e45a64f1346b4e034d853343a336fd75c59aa
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape/vertices/endpoint.h
@@ -0,0 +1,98 @@
+/*
+osap/vertices/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "../core/vertex.h"
+#include "../core/packets.h"
+
+// ---------------------------------------------- Endpoint Routes, extends OSAP Core Routes 
+
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+class EndpointRoute {
+  public: 
+    Route* route;
+    uint8_t ackId = 0;
+    uint8_t ackMode = EP_ROUTEMODE_ACKLESS;
+    EP_ROUTE_STATES state = EP_TX_IDLE;
+    uint32_t lastTxTime = 0;
+    uint32_t timeoutLength;
+    // constructor, 
+    EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength = 1000);
+    // destructor...
+    ~EndpointRoute(void);
+};
+
+// ---------------------------------------------- Endpoints 
+
+// endpoint handler responses must be one of these enum - 
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+// default handlers, 
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len);
+boolean beforeQueryDefault(void);
+
+class Endpoint : public Vertex {
+  public:
+    // local data store & length, 
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t dataLen = 0; 
+    // callbacks: on new data & before a query is written out 
+    EP_ONDATA_RESPONSES (*onData_cb)(uint8_t* data, uint16_t len) = onDataDefault;
+    boolean (*beforeQuery_cb)(void) = beforeQueryDefault;
+    // we override vertex loop, 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // methods,
+    void write(uint8_t* _data, uint16_t len);
+    boolean clearToWrite(void);
+    uint8_t addRoute(Route* _route, uint8_t _mode = EP_ROUTEMODE_ACKLESS, uint32_t _timeoutLength = 1000);
+    // routes, for tx-ing to:
+    EndpointRoute* routes[ENDPOINT_MAX_ROUTES];
+    uint16_t numRoutes = 0;
+    uint16_t lastRouteServiced = 0;
+    uint8_t nextAckID = 77;
+    // base constructor, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+      boolean (*_beforeQuery)(void)
+    );
+    // these are called "delegating constructors" ... best reference is 
+    // here: https://en.cppreference.com/w/cpp/language/constructor 
+    // onData only, 
+    Endpoint(   
+      Vertex* _parent, String _name,
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len)
+    ) : Endpoint ( 
+      _parent, _name, _onData, nullptr
+    ){};
+    // beforeQuery only, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      boolean (*_beforeQuery)(void)
+    ) : Endpoint (
+      _parent, _name, nullptr, _beforeQuery
+    ){};
+    // name only, 
+    Endpoint(   
+      Vertex* _parent, String _name
+    ) : Endpoint (
+      _parent, _name, nullptr, nullptr
+    ){};
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/LICENSE.md b/system/firmware/lpf-loadcell-amp/src/osape_arduino/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/README.md b/system/firmware/lpf-loadcell-amp/src/osape_arduino/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da4c90cb6b618b1b8206b0ddf40a240acbaa4ca7
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/README.md
@@ -0,0 +1,7 @@
+## OSAP Arduino
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+It does not do anything on its own; this one builds helper classes to turn Arduino `Serial` and `Wire` objects into *virtual ports* and *virtual busses* respectively. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.cpp b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8634694ff54bf2f45fdc704f3fd961168f4620bb
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.cpp
@@ -0,0 +1,77 @@
+/*
+arduino-ports/vp_arduinoWire.cpp
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#include "vb_arduinoWire.h"
+
+// static stash: same per instance, 
+uint8_t stash[32];
+uint8_t stashLen = 0;
+
+VBus_ArduinoWire::VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr
+) : VBus ( _parent, _name ) {
+  wire = _wire;
+  ownRxAddr = _ownRxAddr;
+}
+
+void VBus_ArduinoWire::begin(void){
+  wire->begin(ownRxAddr);
+  wire->onReceive(this->onRecieve);
+}
+
+void VBus_ArduinoWire::onRecieve(int count){
+  Wire.readBytes(stash, count);
+  stashLen = count;
+}
+
+void VBus_ArduinoWire::loop(void){
+  // check incoming, 
+  if(stashLen > 0){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      stackLoadSlot(this, VT_STACK_ORIGIN, stash, stashLen);
+    }
+    stashLen = 0;
+  }
+}
+
+void VBus_ArduinoWire::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  digitalWrite(A1, HIGH);
+  // this'll be the big hangup, 
+  if(len > 32) return;
+  // this might guard, if we are already rx'ing... 
+  if(wire->available()) return;
+  // become host, 
+  wire->end();
+  wire->begin();
+  // transmit, 
+  wire->beginTransmission(rxAddr);
+  wire->write(data, len);
+  uint8_t res = wire->endTransmission();
+  // become guest again, 
+  wire->end();
+  wire->begin(ownRxAddr);
+  // check, 
+  //if(res != 0) 
+  // DEBUG("res " + String(res) + " txd " + String(len));
+  digitalWrite(A1, LOW);
+}
+
+boolean VBus_ArduinoWire::cts(uint8_t rxAddr){
+  return true;
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.h b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.h
new file mode 100644
index 0000000000000000000000000000000000000000..b098634545544070b65a46e1106719567f2fbe5b
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vb_arduinoWire.h
@@ -0,0 +1,43 @@
+/*
+arduino-ports/vp_arduinoWire.h
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#ifndef ARDU_WIRELINK_H_
+#define ARDU_WIRELINK_H_
+
+#include <Arduino.h>
+#include <Wire.h>
+#include "../osape/core/vertex.h"
+
+#define WIRELINK_BUFSIZE 255 
+
+class VBus_ArduinoWire : public VBus {
+  public:
+    void begin(void);
+    // -------------------------------- our own loop, cts, and send... 
+    void loop(void) override; 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override; 
+    boolean cts(uint8_t rxAddr) override; 
+    // -------------------------------- data 
+    TwoWire* wire;
+    static void onRecieve(int count);
+    // -------------------------------- constructors
+    VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr);
+};
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.cpp b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f71fe57592eccba322baf9108b38d068a2aed544
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.cpp
@@ -0,0 +1,174 @@
+/*
+arduino-ports/ardu-vport.h
+
+turns serial objects into competent link layers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "vp_arduinoSerial.h"
+#include "./osape/utils/cobs.h"
+#include "../osape/core/osap.h"
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Uart* _uart
+) : VPort ( _parent, _name ){
+  stream = _uart; // should convert Uart* to Stream*, as Uart inherits stream 
+  uart = _uart; 
+}
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Serial_* _usbcdc
+) : VPort ( _parent, _name ){
+  stream = _usbcdc;
+  usbcdc = _usbcdc;
+}
+
+void VPort_ArduinoSerial::begin(uint32_t baudRate){
+  if(uart != nullptr){
+    uart->begin(baudRate);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(baudRate); 
+  }
+}
+
+void VPort_ArduinoSerial::begin(void){
+  if(uart != nullptr){
+    uart->begin(1000000);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(9600);  // baud ignored on cdc begin  
+  }
+}
+
+// link packets are max 256 bytes in length, including the 0 delimiter 
+// structured like:
+// checksum | pck/ack key | pck id | cobs encoded data | 0 
+
+void VPort_ArduinoSerial::loop(void){
+  // byte injestion: think of this like the rx interrupt stage, 
+  while(stream->available()){
+    // read byte into the current stub, 
+    rxBuffer[rxBufferWp ++] = stream->read();
+    if(rxBuffer[rxBufferWp - 1] == 0){
+      // always reset keepalive last-rx time, 
+      lastRxTime = millis();
+      // 1st, we checksum:
+      if(rxBuffer[0] != rxBufferWp){ 
+        OSAP::error("serLink bad checksum, cs: " + String(rxBuffer[0]) + " wp: " + String(rxBufferWp), MINOR);
+      } else {
+        // acks, packs, or broken things 
+        switch(rxBuffer[1]){
+          case SERLINK_KEY_PCK:
+            // dirty guard for retransmitted packets, 
+            if(rxBuffer[2] != lastIdRxd){
+              inAwaitingId = rxBuffer[2]; // stash ID 
+              inAwaitingLen = cobsDecode(&(rxBuffer[3]), rxBufferWp - 2, inAwaiting); // fill inAwaiting 
+            } else {
+              OSAP::error("serLink double rx", MINOR);
+            }
+            break;
+          case SERLINK_KEY_ACK:
+            if(rxBuffer[2] == outAwaitingId){
+              outAwaitingLen = 0;
+            }
+            break;
+          case SERLINK_KEY_KEEPALIVE:
+            // noop, 
+            break;
+          default:
+            // makes no sense, 
+            break;
+        }
+      }
+      // always reset on delimiter, 
+      rxBufferWp = 0;
+    }
+  } // end while-receive 
+
+  // check insertion & genny the ack if we can 
+  if(inAwaitingLen && stackEmptySlot(this, VT_STACK_ORIGIN) && !ackIsAwaiting){
+    stackLoadSlot(this, VT_STACK_ORIGIN, inAwaiting, inAwaitingLen);
+    ackIsAwaiting = true;
+    ackAwaiting[0] = 4;                 // checksum still, innit 
+    ackAwaiting[1] = SERLINK_KEY_ACK;   // it's an ack bruv 
+    ackAwaiting[2] = inAwaitingId;      // which pck r we akkin m8 
+    ackAwaiting[3] = 0;                 // delimiter 
+    inAwaitingLen = 0;
+  }
+
+  // check & execute actual tx 
+  checkOutputStates();
+}
+
+void VPort_ArduinoSerial::send(uint8_t* data, uint16_t len){
+  //digitalWrite(A4, !digitalRead(A4));
+  // double guard?
+  if(!cts()) return;
+  // setup, 
+  outAwaiting[0] = len + 5;               // pck[0] is checksum = len + checksum + cobs start + cobs delimit + ack/pack + id 
+  outAwaiting[1] = SERLINK_KEY_PCK;       // this ones a packet m8 
+  outAwaitingId ++; if(outAwaitingId == 0) outAwaitingId = 1;
+  outAwaiting[2] = outAwaitingId;         // an id     
+  cobsEncode(data, len, &(outAwaiting[3]));  // encode 
+  outAwaiting[len + 4] = 0;               // stuff delimiter, 
+  outAwaitingLen = outAwaiting[0];        // track... 
+  // transmit attempts etc 
+  outAwaitingNTA = 0;
+  outAwaitingLTAT = 0;
+  // try it 
+  checkOutputStates();                    // try / start write 
+}
+
+// we are CTS if outPck is not occupied, 
+boolean VPort_ArduinoSerial::cts(void){
+  return (outAwaitingLen == 0);
+}
+
+// we are open if we've heard back lately, 
+boolean VPort_ArduinoSerial::isOpen(void){
+  return (millis() - lastRxTime < SERLINK_KEEPALIVE_RX_TIME && lastRxTime != 0);
+}
+
+void VPort_ArduinoSerial::checkOutputStates(void){
+  if(ackIsAwaiting && txBufferLen == 0){   // can we ack? 
+    memcpy(txBuffer, ackAwaiting, 4);
+    txBufferLen = 4;
+    lastTxTime = millis();
+    txBufferRp = 0;
+    ackIsAwaiting = false;
+  } else if(outAwaitingLen > 0 && txBufferLen == 0){   // would we be clear to tx ? 
+    // check retransmit cases, 
+    if(outAwaitingLTAT == 0 || outAwaitingLTAT + SERLINK_RETRY_TIME < micros()){
+      memcpy(txBuffer, outAwaiting, outAwaitingLen);
+      outAwaitingLTAT = micros();
+      txBufferLen = outAwaitingLen;
+      lastTxTime = millis();
+      txBufferRp = 0;
+      outAwaitingNTA ++;
+    } 
+    // check if last attempt, 
+    if(outAwaitingNTA >= SERLINK_RETRY_MACOUNT){
+      outAwaitingLen = 0;
+    }
+  } else if (millis() - lastTxTime > SERLINK_KEEPALIVE_TX_TIME && txBufferLen == 0){
+    //OSAP::debug("keepalive-ing " + name + " " + String(isOpen()));
+    memcpy(txBuffer, keepAlivePacket, 3);
+    txBufferLen = 3;
+    lastTxTime = millis();
+  }
+  // finally, we write out so long as we can: 
+  // we aren't guaranteed to get whole pckts out in each fn call 
+  while(stream->availableForWrite() && txBufferLen != 0){
+    // output next byte, 
+    stream->write(txBuffer[txBufferRp ++]);
+    // check for end of buffer; reset transmit states if so 
+    if(txBufferRp >= txBufferLen) {
+      txBufferLen = 0; 
+      txBufferRp = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.h b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa518aabc7e8905a85abf8ec07d4a2138b2f10f2
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_arduino/vp_arduinoSerial.h
@@ -0,0 +1,88 @@
+/*
+arduino-ports/vp_arduinoSerial.h
+
+turns arduino serial objects into competent link layers, for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ARDU_SERLINK_H_
+#define ARDU_SERLINK_H_
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+#define SERLINK_BUFSIZE 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+#define SERLINK_SEGSIZE SERLINK_BUFSIZE - 5
+// packet keys; 
+#define SERLINK_KEY_PCK 170  // 0b10101010
+#define SERLINK_KEY_ACK 171  // 0b10101011
+#define SERLINK_KEY_KEEPALIVE 173 
+// retry settings 
+#define SERLINK_RETRY_MACOUNT 2
+#define SERLINK_RETRY_TIME 100000  // microseconds 
+#define SERLINK_KEEPALIVE_TX_TIME 800 // milliseconds 
+#define SERLINK_KEEPALIVE_RX_TIME 1200 // ms 
+
+#define SERLINK_LIGHT_ON_TIME 100 // in ms 
+
+// note that we use uint8_t write ptrs / etc: and a size of 255, 
+// so we are never dealing w/ wraps etc, god bless 
+
+class VPort_ArduinoSerial : public VPort {
+  public:
+    // arduino std begin 
+    void begin(uint32_t baud);
+    void begin(void);
+    // -------------------------------- our own gd send & cts & loop fns, 
+    void loop(void) override;
+    void checkOutputStates(void);
+    void send(uint8_t* data, uint16_t len) override;
+    boolean cts(void) override;
+    boolean isOpen(void) override;
+    // -------------------------------- Data 
+    // Uart & USB are both Stream classes, 
+    Stream* stream;
+    // we have an overloaded constructor w/ uart or Serial_, the usb class 
+    Uart* uart = nullptr;
+    Serial_* usbcdc = nullptr; 
+    // incoming, always kept clear to receive: 
+    uint8_t rxBuffer[SERLINK_BUFSIZE];
+    uint8_t rxBufferWp = 0;
+    // keepalive state, 
+    uint32_t lastRxTime = 0;
+    uint32_t lastTxTime = 0;
+    uint8_t keepAlivePacket[3] = {3, SERLINK_KEY_KEEPALIVE, 0};
+    // guard on double transmits 
+    uint8_t lastIdRxd = 0;
+    // incoming stash
+    uint8_t inAwaiting[SERLINK_BUFSIZE];
+    uint8_t inAwaitingId = 0;
+    uint8_t inAwaitingLen = 0;
+    // outgoing ack, 
+    uint8_t ackAwaiting[4];
+    boolean ackIsAwaiting = false;
+    // outgoing await,
+    uint8_t outAwaiting[SERLINK_BUFSIZE];
+    uint8_t outAwaitingId = 1;
+    uint8_t outAwaitingLen = 0;
+    uint8_t outAwaitingNTA = 0;
+    unsigned long outAwaitingLTAT = 0;
+    // outgoing buffer,
+    uint8_t txBuffer[SERLINK_BUFSIZE];
+    uint8_t txBufferLen = 0;
+    uint8_t txBufferRp = 0;
+    // -------------------------------- Constructors 
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Uart* _uart);
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Serial_* _usbcdc);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/README.md b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e5a9fae5795a46730372cd9533efa958bc12c2e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/README.md
@@ -0,0 +1,6 @@
+## UART-Clocked Bus Submodule 
+
+https://gitlab.cba.mit.edu/jakeread/ucbus 
+https://github.com/jakeread/ucbus 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.cpp b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2bd443fa0154b4e62e4392611b7afabb257fb
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.cpp
@@ -0,0 +1,510 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "ucBusDipConfig.h"
+#include "../indicators.h"
+#include "../osape/core/osap.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+// last-time-rx'd 
+volatile uint32_t lastRxTime = 0;
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts: not the highest priority (0), just under. 
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  NVIC_SetPriority(SERCOM1_IRQn, 1);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // track keepalive 
+    lastRxTime = millis();
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { 
+    OSAP::error("ucbus-drop outsize numToken rx", MINOR); 
+    return; 
+  }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    OSAP::error("ucbus-drop rx overfull buffer", MINOR);
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || rxCh == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        OSAP::error("ucbus-drop rx FC fails on ch " + String(rxCh), MINOR);
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        // ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        // inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+boolean ucBusDrop_ctrA(void){
+  // likewise
+  return (inBufferLen[0] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+size_t ucBusDrop_readA(uint8_t* dest){
+  if(!ucBusDrop_ctrA()) return 0;
+  // we read out the whole gd thing,
+  size_t len = inBufferLen[0];
+  memcpy(dest, &(inBuffer[0]), len);
+  inBufferLen[0] = 0; // now empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusDrop_isPresent(uint8_t drop){
+  // can't tx anywhere other than to head, 
+  if(drop > 0) return false;
+  return (millis() - lastRxTime < UB_KEEPALIVE_TIME);
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..281f430bd6ced5264fd9607ce8f51a1a9c31cbab
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusDrop.h
@@ -0,0 +1,51 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t* dest);
+boolean ucBusDrop_ctrA(void);
+size_t ucBusDrop_readA(uint8_t* dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+boolean ucBusDrop_isPresent(uint8_t rxAddr);
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.cpp b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..854e488395920dd19b812643282ddbf9c7f3ae25
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.cpp
@@ -0,0 +1,386 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../osape/core/osap.h"
+#include "./utils_samd51/peripheral_nums.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// last-rx'd-time, per drop presence-detect, 
+volatile uint32_t lastRxTime[UB_MAX_DROPS];
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin, also set lastRxTime to zero for each, 
+  for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+    lastRxTime[d] = 0;
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      outBufferLen[ch] = 0;
+      outBufferRp[ch] = 0;
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (channel '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] || ch == 0)){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update keepalive: last we heard from this drop:
+    lastRxTime[rxDrop] = millis();
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { 
+      OSAP::error("ucbus-head outsize numToken rx", MEDIUM); 
+      return; 
+    }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ 
+      OSAP::error("ucbus-head rx FC broken", MEDIUM); 
+      return; 
+    }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      OSAP::error("ucbus-head rx packet too-long", MEDIUM);
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero / are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusHead_isPresent(uint8_t drop){
+  if(drop > UCBUS_MAX_DROPS) return false;
+  return (millis() - lastRxTime[drop] < UB_KEEPALIVE_TIME);
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: channel ID
+  outBuffer[0][0] = channel;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..65f43edcfc482f9656fe30d0bf7f7ea0f9c1eb67
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+boolean ucBusHead_isPresent(uint8_t drop); // have we heard from this drop recently ? 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusMacros.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusMacros.h
new file mode 100644
index 0000000000000000000000000000000000000000..72f3f0c0b60e6c7efac390db2e6db4be7e9b133a
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucBusMacros.h
@@ -0,0 +1,127 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "./ucbus_config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// time-until-considered-dead, in ms  
+#define UB_KEEPALIVE_TIME 200 
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL 2 // A: 0, B: 1, C: 2
+#define UB_RXPERIPHERAL 2
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.cpp b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08742fdc5cda9435f8ad54b76a4f85c81c433b1e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..97ec2b5750e86bbd6d98acd3ef42c02b489240f4
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.cpp b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a49901b40751839e972e4f5d778179834dba1868
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.cpp
@@ -0,0 +1,95 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusDrop.h"
+#include "../osape/core/osap.h"
+
+// badness, direct write in future 
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusDrop::VBus_UCBusDrop(Vertex* _parent, String _name
+): VBus(_parent, _name){
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusDrop::begin(void){
+  ucBusDrop_setup(true, 0);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::begin(uint8_t _ownRxAddr){
+  ucBusDrop_setup(false, _ownRxAddr);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::loop(void){
+  // can we shift-in from channel a / broadcast messages ?
+  // also... stack 'em from the broadcast channel first, typically higher priority 
+  if(ucBusDrop_ctrA()){
+    // and if we have an empty space... 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+    // get len & strip out the broadcastChannel, which was stuffed at [0]
+    uint16_t len = ucBusDrop_readA(_tempBuffer);
+    injestBroadcastPacket(&(_tempBuffer[1]), len - 1, _tempBuffer[0]);
+    }
+  }
+  // can we shift-in from channel b / directed messages ? 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+void VBus_UCBusDrop::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    OSAP::error("ubd tx while not clear", MEDIUM);
+  }
+}
+
+boolean VBus_UCBusDrop::cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  return (rxAddr == 0 && ucBusDrop_ctsB());
+}
+
+void VBus_UCBusDrop::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  OSAP::debug("Broadcast is unwritten");
+}
+
+boolean VBus_UCBusDrop::ctb(uint8_t broadcastChannel){
+  OSAP::debug("Bus Drop CTB is unwritten");
+  return false;
+}
+
+boolean VBus_UCBusDrop::isOpen(uint8_t rxAddr){
+  return ucBusDrop_isPresent(rxAddr);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4333e6491b0439d01ae4bc480bce37af864f5
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusDrop.h
@@ -0,0 +1,41 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VBUS_UCBUS_HEAD_H_
+#define VBUS_UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusDrop : public VBus {
+  public:
+    void begin(void);
+    void begin(uint8_t _ownRxAddr);
+    void loop(void) override;
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr);
+    VBus_UCBusDrop(Vertex* _parent, String _name);
+};
+
+#endif 
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.cpp b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0e5cd5676e138fa0c17215c075181594ff48ac
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.cpp
@@ -0,0 +1,93 @@
+/*
+osap/vb_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusHead.h"
+#include "../osape/core/osap.h"
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusHead::VBus_UCBusHead(Vertex* _parent, String _name
+): VBus (_parent, _name) {
+  // report our address size,
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusHead::begin(void){
+  // start ucbus
+  ucBusHead_setup(); 
+}
+
+void VBus_UCBusHead::loop(void){
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      if (stackEmptySlot(this, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+void VBus_UCBusHead::timerISR(void){
+  ucBusHead_timerISR();
+}
+
+void VBus_UCBusHead::send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    OSAP::error("attempt to busf from head to self", MEDIUM);
+  } else {  
+    ucBusHead_transmitB(data, len, rxAddr);
+  }
+}
+
+boolean VBus_UCBusHead::cts(uint8_t rxAddr){
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void VBus_UCBusHead::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  ucBusHead_transmitA(data, len, broadcastChannel);
+}
+
+boolean VBus_UCBusHead::ctb(uint8_t broadcastChannel){
+  return ucBusHead_ctsA();
+}
+
+boolean VBus_UCBusHead::isOpen(uint8_t rxAddr){
+  return ucBusHead_isPresent(rxAddr);
+}
+
+#endif 
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.h b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfb7829f135f8ea04f193d3657f38cb15ea63cfa
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/osape_ucbus/vb_ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/vb_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusHead : public VBus {
+  public:
+    void begin(void);
+    // loop to ferry data, 
+    void loop(void) override;
+    // fast loop, needs to be called in ~ 10kHz ISR 
+    void timerISR(void);
+    // ... bus : osap API 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr) override;
+    // -------------------------------- Constructors 
+    VBus_UCBusHead(Vertex* _parent, String _name);
+};
+
+#endif
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/ucbus_config.h b/system/firmware/lpf-loadcell-amp/src/ucbus_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..eee24b0289a8649bc3543cd3dc759ebd0b131d97
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/ucbus_config.h
@@ -0,0 +1,29 @@
+/*
+ucbus_confi.h
+
+config options for an ucbus instance 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_CONFIG_H_
+#define UCBUS_CONFIG_H_
+
+#define UCBUS_MAX_DROPS 32 
+#define UCBUS_IS_DROP 
+//#define UCBUS_IS_HEAD 
+
+#define UCBUS_BAUD 2 
+
+#define UCBUS_IS_D51
+// #define UCBUS_IS_D21
+
+#define UCBUS_ON_OSAP 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/utils_samd51/README.md b/system/firmware/lpf-loadcell-amp/src/utils_samd51/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a5e4922e5be8001ad756c57dc6cd5c934ca1572e
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/utils_samd51/README.md
@@ -0,0 +1,3 @@
+## ATSAMD51 Utes
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.cpp b/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387cbaad46e7f17a5f553c446a47558abbc20bc7
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.cpp
@@ -0,0 +1,129 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "clock_utils.h"
+#include "../indicators.h"
+
+/*
+// I used to have this singleton stuff here, but I think
+// since I am using the extern... I have no need for it, 
+D51ClockUtils* D51ClockUtils::instance = 0;
+
+D51ClockUtils* D51ClockUtils::getInstance(void){
+    if(instance == 0){
+        instance = new D51ClockUtils();
+    }
+    return instance;
+}
+
+D51ClockUtils* D51ClockUtils = D51ClockUtils::getInstance();
+*/
+
+D51ClockUtils* d51ClockUtils;
+
+D51ClockUtils::D51ClockUtils(){}
+
+void D51ClockUtils::setup_16mhz_xtal(void){
+    if(mhz_xtal_is_setup) return; // already done, 
+    // let's make a clock w/ that xtal:
+    OSCCTRL->XOSCCTRL[0].bit.RUNSTDBY = 0;
+    OSCCTRL->XOSCCTRL[0].bit.XTALEN = 1;
+    // set oscillator current..
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_IMULT(4) | OSCCTRL_XOSCCTRL_IPTAT(3);
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_STARTUP(5);
+    OSCCTRL->XOSCCTRL[0].bit.ENALC = 1;
+    OSCCTRL->XOSCCTRL[0].bit.ENABLE = 1;
+    // make the peripheral clock available on this ch 
+    GCLK->GENCTRL[MHZ_XTAL_GCLK_NUM].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_XOSC0) | GCLK_GENCTRL_GENEN;  // GCLK_GENCTRL_SRC_DFLL
+    while (GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(MHZ_XTAL_GCLK_NUM)){
+        //DEBUG2PIN_TOGGLE;
+    };
+    mhz_xtal_is_setup = true;
+}
+
+void D51ClockUtils::start_ticker_a(uint32_t us){
+    //now using 120mHz main clock (gen(0)) instead of xtal, 
+    //setup_16mhz_xtal();
+    // ok
+    TC0->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC1->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0 | MCLK_APBAMASK_TC1;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC0->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC1->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC0->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC1->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC0->COUNT32.INTENSET.bit.MC0 = 1;
+    TC0->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC0->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 100kHz, ok? 
+    if(us < 10) us = 10;
+    // 120 / 2 -> 60 ticks per us, 
+    TC0->COUNT32.CC[0].reg = 60 * us;
+    // enable, sync for enable write
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC0->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC1->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC0_IRQn);
+    NVIC_SetPriority(TC0_IRQn, 2);
+}
+
+void D51ClockUtils::set_ticker_a_priority(uint32_t prio){
+    if(prio > 3) prio = 3;
+    NVIC_SetPriority(TC0_IRQn, prio);
+}
+
+void D51ClockUtils::start_ticker_b(uint32_t us){
+    setup_16mhz_xtal();
+    // ok
+    TC2->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC3->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBBMASK.reg |= MCLK_APBBMASK_TC2 | MCLK_APBBMASK_TC3;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC2_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC3_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC2->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC3->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC2->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC3->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC2->COUNT32.INTENSET.bit.MC0 = 1;
+    TC2->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC2->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 1MHz, ok? 
+    if(us < 8) us = 8;
+    TC2->COUNT32.CC[0].reg = 8 * us;
+    // enable, sync for enable write
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC2->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC3->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC2_IRQn);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.h b/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1f9e7472c034dc3a1aee6fc995eb65043d660
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/utils_samd51/clock_utils.h
@@ -0,0 +1,45 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef CLOCKS_D51_H_
+#define CLOCKS_D51_H_
+
+#include <Arduino.h>
+
+#define MHZ_XTAL_GCLK_NUM 9
+
+class D51ClockUtils {
+    private:
+        static D51ClockUtils* instance;
+    public:
+        D51ClockUtils();
+        static D51ClockUtils* getInstance(void);
+        // xtal
+        volatile boolean mhz_xtal_is_setup = false;
+        uint32_t mhz_xtal_gclk_num = 9;
+        void setup_16mhz_xtal(void);
+        // uses TC0 and TC1 as 32 bit TC
+        // pickup TC0_Handler(void){}
+        // do in handler: 
+        // TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+        // TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+        // us: requested timer period 
+        void start_ticker_a(uint32_t us);
+        void set_ticker_a_priority(uint32_t prio);
+        // uses TC2 and TC3 as 32 bit TC 
+        // pickup on TC2_Handler(void){}
+        void start_ticker_b(uint32_t us);
+};
+
+extern D51ClockUtils* d51ClockUtils;
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/utils_samd51/peripheral_nums.h b/system/firmware/lpf-loadcell-amp/src/utils_samd51/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/utils_samd51/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/src/utils_samd51/pin_macros.h b/system/firmware/lpf-loadcell-amp/src/utils_samd51/pin_macros.h
new file mode 100644
index 0000000000000000000000000000000000000000..89418657d8a481cb20ec3532cbd3ef0488dda521
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/src/utils_samd51/pin_macros.h
@@ -0,0 +1,13 @@
+#ifndef PIN_MACROS_D51_H_
+#define PIN_MACROS_D51_H_
+
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+#define PIN_SETUP_INPUT(port, pin) PORT->Group[port].DIRCLR.reg = PIN_BM(pin); PORT->Group[port].PINCFG[pin].reg = PORT_PINCFG_INEN
+#define PIN_SETUP_PULLEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PULLEN = 1
+#define PIN_SETUP_PMUXEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PMUXEN = 1
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-loadcell-amp/test/README b/system/firmware/lpf-loadcell-amp/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..b94d0890faa00a63737892509a5ca77ad3bdc6c3
--- /dev/null
+++ b/system/firmware/lpf-loadcell-amp/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Unit Testing and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/page/plus/unit-testing.html
diff --git a/system/firmware/lpf-modular-motion-head/.gitignore b/system/firmware/lpf-modular-motion-head/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/system/firmware/lpf-modular-motion-head/.vscode/extensions.json b/system/firmware/lpf-modular-motion-head/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/system/firmware/lpf-modular-motion-head/include/README b/system/firmware/lpf-modular-motion-head/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/system/firmware/lpf-modular-motion-head/lib/README b/system/firmware/lpf-modular-motion-head/lib/README
new file mode 100644
index 0000000000000000000000000000000000000000..6debab1e8b4c3faa0d06f4ff44bce343ce2cdcbf
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/system/firmware/lpf-modular-motion-head/platformio.ini b/system/firmware/lpf-modular-motion-head/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..50208f0cadb2e924ce3a661e45b5c88422dc71af
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/platformio.ini
@@ -0,0 +1,14 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:adafruit_feather_m4]
+platform = atmelsam
+board = adafruit_feather_m4
+framework = arduino
diff --git a/system/firmware/lpf-modular-motion-head/src/indicators.h b/system/firmware/lpf-modular-motion-head/src/indicators.h
new file mode 100644
index 0000000000000000000000000000000000000000..24d74a0c73094630b5cc48797ba1b85c2be3102a
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/indicators.h
@@ -0,0 +1,55 @@
+// circuit specific indicators: modular-motion-head 2021-08-26 
+
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+
+#define CLKLIGHT_ON PIN_LO(0, 27) 
+#define CLKLIGHT_OFF PIN_HI(0, 27)
+#define CLKLIGHT_TOGGLE PIN_TGL(0, 27)
+#define CLKLIGHT_SETUP PIN_SETUP_OUTPUT(0, 27); CLKLIGHT_OFF
+
+#define ERRLIGHT_ON PIN_LO(1, 8)
+#define ERRLIGHT_OFF PIN_HI(1, 8)
+#define ERRLIGHT_TOGGLE PIN_TGL(1, 8)
+#define ERRLIGHT_SETUP PIN_SETUP_OUTPUT(1, 8); ERRLIGHT_OFF
+
+// breakout debugs are pin-to-gnd, 
+// PA13, PA12, PB15, PB14, PA04 
+
+#define DEBUG1PIN_ON PIN_LO(0, 13)
+#define DEBUG1PIN_OFF PIN_HI(0, 13)
+#define DEBUG1PIN_HI PIN_HI(0, 13)
+#define DEBUG1PIN_LO PIN_LO(0, 13)
+#define DEBUG1PIN_TOGGLE PIN_TGL(0, 13)
+#define DEBUG1PIN_SETUP PIN_SETUP_OUTPUT(0, 13); PIN_HI(0, 13)
+
+#define DEBUG2PIN_ON PIN_LO(0, 12)
+#define DEBUG2PIN_OFF PIN_HI(0, 12)
+#define DEBUG2PIN_HI PIN_HI(0, 12)
+#define DEBUG2PIN_LO PIN_LO(0, 12)
+#define DEBUG2PIN_TOGGLE PIN_TGL(0, 12)
+#define DEBUG2PIN_SETUP PIN_SETUP_OUTPUT(0, 12); PIN_HI(0, 12)
+
+#define DEBUG3PIN_ON PIN_LO(1, 15)
+#define DEBUG3PIN_OFF PIN_HI(1, 15)
+#define DEBUG3PIN_HI PIN_HI(1, 15)
+#define DEBUG3PIN_LO PIN_LO(1, 15)
+#define DEBUG3PIN_TOGGLE PIN_TGL(1, 15)
+#define DEBUG3PIN_SETUP PIN_SETUP_OUTPUT(1, 15); PIN_HI(1, 15)
+
+#define DEBUG4PIN_ON PIN_LO(1, 14)
+#define DEBUG4PIN_OFF PIN_HI(1, 14)
+#define DEBUG4PIN_HI PIN_HI(1, 14)
+#define DEBUG4PIN_LO PIN_LO(1, 14)
+#define DEBUG4PIN_TOGGLE PIN_TGL(1, 14)
+#define DEBUG4PIN_SETUP PIN_SETUP_OUTPUT(1, 14); PIN_HI(1, 14)
+
+#define DEBUG5PIN_ON PIN_LO(0, 4)
+#define DEBUG5PIN_OFF PIN_HI(0, 4)
+#define DEBUG5PIN_HI PIN_HI(0, 4)
+#define DEBUG5PIN_LO PIN_LO(0, 4)
+#define DEBUG5PIN_TOGGLE PIN_TGL(0, 4)
+#define DEBUG5PIN_SETUP PIN_SETUP_OUTPUT(0, 4); PIN_HI(0, 4)
diff --git a/system/firmware/lpf-modular-motion-head/src/main.cpp b/system/firmware/lpf-modular-motion-head/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..46df4ac759f61554e372a41a226e3cd41e44f678
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/main.cpp
@@ -0,0 +1,304 @@
+#include <Arduino.h>
+
+#include "indicators.h"
+#include "utils_samd51/clock_utils.h"
+
+#include "osape/core/osap.h"
+#include "osape/vertices/endpoint.h"
+
+#include "osape_arduino/vp_arduinoSerial.h"
+
+#include "osape_ucbus/vb_ucBusHead.h"
+
+// -------------------------------------------------------- OSAP ENDPOINTS SETUP
+
+OSAP osap("motion-head");
+
+VPort_ArduinoSerial vpUSBSer(&osap, "arduinoUSBSerial", &Serial);   // 0
+
+VBus_UCBusHead vbUCBusHead(&osap, "ucBusHead");                     // 1
+
+/*
+
+// -------------------------------------------------------- 2: States
+
+EP_ONDATA_RESPONSES onStateData(uint8_t* data, uint16_t len){
+  ERRLIGHT_TOGGLE;
+  // check for partner-config badness, 
+  if(len != AXL_NUM_DOF * 4 + 2){ OSAP::error("state req has bad DOF count"); return EP_ONDATA_REJECT; }
+  // we have accel, rate, posn data, 
+  dofs targ;
+  uint16_t rptr = 0;
+  uint8_t mode = data[rptr ++];
+  uint8_t set = data[rptr ++];
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    targ.axis[a] = ts_readFloat32(data, &rptr);
+  }
+  // set or target?
+  if(set){
+    switch(mode){
+      case AXL_MODE_POSITION:
+        if(axl_isMoving()){
+          OSAP::error("AXL can't set pos while moving");
+          break;
+        }
+        axl_setPosition(targ);
+        break;
+      default:
+        OSAP::error("we can only 'set' position, others are targs");
+        break;
+    }
+  } else {
+    switch(mode){
+      case AXL_MODE_ACCEL:
+        axl_setAccelTarget(targ);
+        break;
+      case AXL_MODE_VELOCITY:
+        axl_setVelocityTarget(targ);
+        break;
+      case AXL_MODE_POSITION:
+        axl_setPositionTarget(targ);
+        break;
+      default:
+        OSAP::error("AXL state targ has bad / unrecognized mode");
+        break;
+    }
+  }
+  // since we routinely update it w/ actual states (not requests) 
+  return EP_ONDATA_REJECT;
+}
+
+Endpoint statesEP(&osap, "states", onStateData);
+
+void updateStatesEP(void){
+  uint8_t numBytes = AXL_NUM_DOF * 4 * 3 + 2;
+  uint8_t stash[numBytes]; uint16_t wptr = 0;
+  stash[wptr ++] = axl_getMode();
+  axl_isMoving() ? stash[wptr ++] = 1 : stash[wptr ++] = 0;
+  dofs temp = axl_getPositions();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    ts_writeFloat32(temp.axis[a], stash, &wptr);
+  }
+  temp = axl_getVelocities();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    ts_writeFloat32(temp.axis[a], stash, &wptr);
+  }
+  temp = axl_getAccelerations();
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    ts_writeFloat32(temp.axis[a], stash, &wptr);
+  }
+  statesEP.write(stash, numBytes);
+}
+
+// -------------------------------------------------------- 3: Halt
+
+uint32_t haltLightOnTime = 0;
+
+EP_ONDATA_RESPONSES onHaltData(uint8_t* data, uint16_t len){
+  axl_halt();
+  ERRLIGHT_ON;
+  haltLightOnTime = millis();
+  return EP_ONDATA_REJECT;
+}
+
+Endpoint haltEP(&osap, "halt", onHaltData);
+
+// -------------------------------------------------------- 4: Moves -> Queue
+
+EP_ONDATA_RESPONSES onMoveData(uint8_t* data, uint16_t len){
+  // this (and states-input) could watch <len> to make sure that 
+  // this code & transmitter code are agreeing on how many DOFs are specd 
+  if(axl_hasQueueSpace()){
+    uint16_t rptr = 0;
+    float rate = ts_readFloat32(data, &rptr);
+    dofs targ;
+    for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+      targ.axis[a] = ts_readFloat32(data, &rptr);
+    }
+    axl_addMoveToQueue(targ, rate);
+    return EP_ONDATA_ACCEPT;
+  } else {
+    return EP_ONDATA_WAIT;
+  }
+}
+
+Endpoint moveEP(&osap, "moves", onMoveData);
+
+// -------------------------------------------------------- 5: AXL Settings
+
+EP_ONDATA_RESPONSES onAXLSettingsData(uint8_t* data, uint16_t len){
+  // jd, then pairs of accel & vel limits,
+  float jd;
+  dofs accelLimits;
+  dofs velLimits;
+  uint16_t rptr = 0;
+  jd = ts_readFloat32(data, &rptr);
+  for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+    accelLimits.axis[a] = ts_readFloat32(data, &rptr);
+    velLimits.axis[a] = ts_readFloat32(data, &rptr);
+  }
+  axl_setJunctionDeviation(jd);
+  axl_setAccelLimits(accelLimits);
+  axl_setVelLimits(velLimits);
+  return EP_ONDATA_ACCEPT;
+}
+
+Endpoint axlSettingsEP(&osap, "axlSettings", onAXLSettingsData);
+
+*/
+
+// -------------------------------------------------------- POWER MODES 
+
+#define V5_ON PIN_HI(0, 11)
+#define V5_OFF PIN_LO(0, 11)
+#define V5_SETUP PIN_SETUP_OUTPUT(0, 11); PIN_LO(0, 11)
+#define V24_ON PIN_HI(0, 10)
+#define V24_OFF PIN_LO(0, 10)
+#define V24_SETUP PIN_SETUP_OUTPUT(0, 10); PIN_LO(0, 10)
+
+// 5V Switch on PA11, 24V Switch on PA10
+/*  5v  | 24v | legal 
+    0   | 0   | yes
+    1   | 0   | yes 
+    0   | 1   | no 
+    1   | 1   | yes 
+
+lol, pretty easy I guess: just no 24v when no 5v...
+we also want to turn on in-order though: 5v first, then 24v, and 24v off, then 5v 
+*/
+
+// track states 
+boolean state5V = false;
+boolean state24V = false;
+
+void publishPowerStates(void);
+
+// make changes 
+void powerStateUpdate(boolean st5V, boolean st24V){
+  // guard against bad state
+  if(st24V && !st5V) st24V = false;
+  // check order-of-flip... if 5v is turning off, we will turn 24v first, 
+  // in all other scenarios, we flip 5v first 
+  if(state5V && !st5V){
+    state24V = st24V; state5V = st5V;
+    // publish, 24v first, and allow charge to leave... 
+    state24V ? V24_ON : V24_OFF;
+    delay(50);
+    state5V ? V5_ON : V5_OFF;
+  } else {
+    state24V = st24V; state5V = st5V;
+    // publish, 5v first, and allow some bring-up... 
+    state5V ? V5_ON : V5_OFF;
+    delay(10);
+    state24V ? V24_ON : V24_OFF;
+  }
+  // now ... would like to write to the endpoint 
+  publishPowerStates();
+}
+
+EP_ONDATA_RESPONSES onPowerData(uint8_t* data, uint16_t len){
+  // read requested states out 
+  boolean st5V, st24V;
+  uint16_t rptr = 0;
+  ts_readBoolean(&st5V, data, &rptr);
+  ts_readBoolean(&st24V, data, &rptr);
+  // run the update against our statemachine 
+  powerStateUpdate(st5V, st24V);
+  // here's a case where we'll never want to let senders to 
+  // update our internal state, so we just return 
+  return EP_ONDATA_REJECT;
+  // this means that the endpoint's data store will remain unchanged (from the write) 
+  // but remains true to what was written in when we updated w/ the powerStateUpdate fn... 
+}
+
+Endpoint powerEp(&osap, "powerSwitches", onPowerData);      // 6: Power Switches 
+
+void publishPowerStates(void){
+  uint8_t powerData[2];
+  uint16_t wptr = 0;
+  ts_writeBoolean(state5V, powerData, &wptr);
+  ts_writeBoolean(state24V, powerData, &wptr);
+  powerEp.write(powerData, 2);
+}
+
+// -------------------------------------------------------- 8: Precalcd-move-adder / producer 
+
+// Endpoint precalculatedMoveEP(&osap, "precalculatedMoveOutput");
+
+// -------------------------------------------------------- SETUP 
+
+void setup() {
+  ERRLIGHT_SETUP;
+  CLKLIGHT_SETUP;
+  DEBUG1PIN_SETUP;
+  DEBUG2PIN_SETUP;
+  DEBUG3PIN_SETUP;
+  DEBUG4PIN_SETUP;
+  DEBUG5PIN_SETUP;
+  // setup the power stuff 
+  V5_SETUP;
+  V24_SETUP;
+  powerStateUpdate(false, false);
+  // osap
+  vpUSBSer.begin();
+  vbUCBusHead.begin();
+  // startup axl, 
+  // axl_setup(); 
+  // bus runs on 10kHz ticker 
+  d51ClockUtils->start_ticker_a(1000000/10000); 
+  // turn 5v on by default,  
+  powerStateUpdate(true, true);
+}
+
+unsigned long epUpdateInterval = 250; // ms 
+unsigned long lastUpdate = 0;
+uint16_t moveDataLen = 0;
+uint8_t moveBuffer[128];
+
+void loop() {
+  // main recursive osap loop:
+  osap.loop();
+  // check for axl broadcast data, 
+  // if(precalculatedMoveEP.clearToWrite()){
+  //   moveDataLen = axl_netLoop(moveBuffer);
+  //   if(moveDataLen){
+  //     precalculatedMoveEP.write(moveBuffer, moveDataLen);
+  //   }
+  // }
+  // run 10Hz endpoint update:
+  if(millis() > lastUpdate + epUpdateInterval){
+    lastUpdate = millis();
+    DEBUG5PIN_TOGGLE;
+    // updateStatesEP();
+  }
+  // if(haltLightOnTime + 250 < millis()){
+  //   ERRLIGHT_OFF;
+  // }
+} // end loop 
+
+// noop for actuator-free-zone 
+void axl_onPositionDelta(uint8_t axis, float delta){}
+void axl_limitSetup(void){}
+boolean axl_checkLimit(void){ return true; }
+
+// runs on period defined by timer_a setup: 
+volatile uint32_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+
+void TC0_Handler(void){
+  // runs at period established above... 
+  TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+  TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+  DEBUG1PIN_HI;
+  // do bus action first: want downstream clocks to be deterministic-ish
+  vbUCBusHead.timerISR();
+  // do axl integration, 
+  // axl_integrator();
+  // do blinking, lol
+  timeBlink ++;
+  if(timeBlink > 500){
+    CLKLIGHT_TOGGLE;
+    timeBlink = 0; 
+  }
+  DEBUG1PIN_LO;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osap_config.h b/system/firmware/lpf-modular-motion-head/src/osap_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..d0543315a010c660918630779ba9fd196e0311e1
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osap_config.h
@@ -0,0 +1,33 @@
+/*
+osap_config.h
+
+config options for an osap-embedded build 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_CONFIG_H_
+#define OSAP_CONFIG_H_
+
+// size of vertex stacks, lenght, then count,
+#define VT_SLOTSIZE 256
+#define VT_STACKSIZE 3  // must be >= 2 for ringbuffer operation 
+#define VT_MAXCHILDREN 16
+#define VT_MAXITEMSPERTURN 8
+
+// max # of endpoints that could be spawned here,
+#define MAX_CONTEXT_ENDPOINTS 64
+
+// count of routes each endpoint can have, 
+#define ENDPOINT_MAX_ROUTES 4
+#define ENDPOINT_ROUTE_MAX_LEN 64 
+
+#define VBUS_MAX_BROADCAST_CHANNELS 64 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/LICENSE.md b/system/firmware/lpf-modular-motion-head/src/osape/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/README.md b/system/firmware/lpf-modular-motion-head/src/osape/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c94ebaff92a9980dbc93aa25047846ee4aa64e0
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/README.md
@@ -0,0 +1,5 @@
+## OSAP Embedded 
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/loop.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/loop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c050974467d2fc95677d72f2e2da3b6608a0f588
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/loop.cpp
@@ -0,0 +1,255 @@
+/*
+osap/osapLoop.cpp
+
+main osap op: whips data vertex-to-vertex
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "loop.h"
+#include "packets.h"
+#include "osap.h"
+
+#define MAX_ITEMS_PER_LOOP 32
+//#define LOOP_DEBUG
+
+// we'll stack up to 64 messages to handle per loop, 
+// more items would cause issues: will throw errors and design circular looping at that point 
+stackItem* itemList[MAX_ITEMS_PER_LOOP];
+uint16_t itemListLen = 0;
+
+void listSetupRecursor(Vertex* vt){
+  // run the vertex' loop... but not if it's the root, yar 
+  if(vt->type != VT_TYPE_ROOT) vt->loop();
+  // for each input / output stack, try to collect all items... 
+  // alright I'm doing this collect... but want a kind of pickup-where-you-left-off thing, 
+  // so that we can have a fixed-length loop, i.e. 64 items per, but still do fairness... 
+  // otherwise our itemList has to be large enough to carry potentially every single item ? 
+  for(uint8_t od = 0; od < 2; od ++){
+    uint8_t count = stackGetItems(vt, od, &(itemList[itemListLen]), MAX_ITEMS_PER_LOOP - itemListLen);
+    itemListLen += count;
+  }
+  // recurse children...
+  for(uint8_t c = 0; c < vt->numChildren; c ++){
+    listSetupRecursor(vt->children[c]);
+  }
+}
+
+// sort-in-place based on time-to-death, 
+void listSort(stackItem** list, uint16_t listLen){
+  // write each item's time-to-death, 
+  uint32_t now = millis();
+  for(uint16_t i = 0; i < listLen; i ++){
+    list[i]->timeToDeath = ts_readUint16(list[i]->data, 0) - (now - list[i]->arrivalTime);
+  }
+  // also... vertex arrivalTime should be uint32_t milliseconds of arrival... 
+  #warning not-yet sorted... 
+}
+
+// this handles internal transport... checking for errors along paths, and running flowcontrol 
+// returns true to wipe current item, false to leave-in-wait, 
+boolean internalTransport(stackItem* item, uint16_t ptr){
+  // we walk thru our little internal tree here, 
+  Vertex* vt = item->vt;
+  // ptr for the walk, use item->data[ptr] == PK_INSTRUCTION, not PK_PTR, 
+  uint16_t fwdPtr = ptr + 1;
+  // count # of ops, 
+  uint8_t opCount = 0;
+  // for a max. of 16 fwd steps, 
+  for(uint8_t s = 0; s < 16; s ++){
+    uint16_t arg = readArg(item->data, fwdPtr);
+    switch(PK_READKEY(item->data[fwdPtr])){
+      // ---------------------------------------- Internal Dir Cases 
+      case PK_SIB:
+        // check validity of route & shift our reference vt,
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during sib transport"); return true;
+        } else if (arg >= vt->parent->numChildren){
+          OSAP::error("no sibling " + String(arg) + " at " + vt->name + " during sib transport"); return true;
+        } else {
+          // this is it: we go fwds to this vt & end-of-switch statements increment ptrs
+          vt = vt->parent->children[arg];
+        }
+        break;
+      case PK_PARENT:
+        if(vt->parent == nullptr){
+          OSAP::error("no parent at " + vt->name + " during parent transport"); return true;
+        } else {
+          // likewise... 
+          vt = vt->parent;
+        }
+        break;
+      case PK_CHILD:
+        if(arg >= vt->numChildren){
+          OSAP::error("no child " + String(arg) + " at " + vt->name + " during child transport"); return true;
+        } else {
+          // again, just walk fwds... 
+          vt = vt->children[arg];
+        }
+        break;
+      // ---------------------------------------- Terminal / Exit Cases 
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD: 
+      case PK_DEST:
+      case PK_PINGREQ:
+      case PK_SCOPEREQ:
+      case PK_LLESCAPE:
+        // check / transport...
+        if(stackEmptySlot(vt, VT_STACK_DESTINATION)){
+          // walk the ptr fwds, 
+          walkPtr(item->data, item->vt, opCount, ptr);
+          // ingest at the new place, 
+          stackLoadSlot(vt, VT_STACK_DESTINATION, item->data, item->len);
+          // return true to clear it out, 
+          return true;
+        } else {
+          return false; 
+        }
+      default:
+        OSAP::error("internal transport failure, ptr walk ends at unknown key");
+        return true;
+    } // end switch 
+    fwdPtr += 2;
+    opCount ++;
+  } // end max-16-steps, 
+  // if we're past all 16 and didn't hit a terminal, pckt is eggregiously long, rm it 
+  return true;
+}
+
+// -------------------------------------------------------- LOOP Begins Here 
+
+// ... would be breadth-first, ideally 
+void osapLoop(Vertex* root){
+  // we want to build a list of items, recursing through... 
+  itemListLen = 0;
+  listSetupRecursor(root);
+  // check now if items are nearly oversized...
+  // see notes in the log from 2022-06-22 if this error occurs, 
+  if(itemListLen >= MAX_ITEMS_PER_LOOP - 2){
+    OSAP::error("loop items exceeds " + String(MAX_ITEMS_PER_LOOP) + ", breaking per-loop transport properties... pls fix", HALTING);
+  }
+  // stash high-water mark,
+  if(itemListLen > OSAP::loopItemsHighWaterMark) OSAP::loopItemsHighWaterMark = itemListLen;
+  // log 'em 
+  // OSAP::debug("list has " + String(itemListLen) + " elements", LOOP);
+  // otherwise we can carry on... the item should be sorted, global vars, 
+  listSort(itemList, itemListLen);
+  // then we can handle 'em one by one 
+  for(uint16_t i = 0; i < itemListLen; i ++){
+    osapItemHandler(itemList[i]);
+  }
+}
+
+void osapItemHandler(stackItem* item){
+  // clear dead items, 
+  if(item->timeToDeath < 0){
+    OSAP::debug(  "item at " + item->vt->name + " times out w/ " + String(item->timeToDeath) + 
+                  " ms to live, of " + String(ts_readUint16(item->data, 0)) + " ttl", LOOP);
+    stackClearSlot(item);
+    return;
+  }
+  // get a ptr for the item, 
+  uint16_t ptr = 0;
+  if(!findPtr(item->data, &ptr)){    
+    OSAP::error("item at " + item->vt->name + " unable to find ptr, deleting...");
+    stackClearSlot(item);
+    return;
+  }
+  // now the handle-switch, item->data[ptr] = PK_PTR, we switch on instruction which is behind that, 
+  switch(PK_READKEY(item->data[ptr + 1])){
+    // ------------------------------------------ Terminal / Destination Switches 
+    case PK_DEST:
+      item->vt->destHandler(item, ptr);
+      break;
+    case PK_PINGREQ:
+      item->vt->pingRequestHandler(item, ptr);
+      break;
+    case PK_SCOPEREQ:
+      item->vt->scopeRequestHandler(item, ptr);
+      break;
+    case PK_PINGRES:
+    case PK_SCOPERES:
+      OSAP::error("ping or scope request issued to " + item->vt->name + " not handling those in embedded", MEDIUM);
+      stackClearSlot(item);
+      break;
+    // ------------------------------------------ Internal Transport 
+    case PK_SIB:
+    case PK_PARENT:
+    case PK_CHILD:  // transport handler returns true if msg should be wiped, false if it should be cycled
+      if(internalTransport(item, ptr)){
+        stackClearSlot(item);
+      }
+      break;
+    // ------------------------------------------ Network Transport 
+    case PK_PFWD:
+      // port forward...
+      if(item->vt->vport == nullptr){
+        OSAP::error("pfwd to non-vport " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        if(item->vt->vport->cts()){
+          // walk one step, but only if fn returns true (having success) 
+          if(walkPtr(item->data, item->vt, 1, ptr)) item->vt->vport->send(item->data, item->len);
+          stackClearSlot(item);
+        } else {
+          // failed to send this turn (flow controlled), will return here next round 
+        }
+      }
+      break;
+    case PK_BFWD:
+    case PK_BBRD:
+      // bus forward / bus broadcast: 
+      if(item->vt->vbus == nullptr){
+        OSAP::error("bfwd to non-vbus " + item->vt->name, MEDIUM);
+        stackClearSlot(item);
+      } else {
+        // arg is rxAddr for bus-forwards, is broadcastChannel for bus-broadcast, 
+        uint16_t arg = readArg(item->data, ptr + 1);
+        if(item->data[ptr + 1] == PK_BFWD){
+          if(item->vt->vbus->cts(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              item->vt->vbus->send(item->data, item->len, arg);
+            } else {
+              OSAP::error("bfwd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bfwd (flow controlled), returning here next round... 
+          }
+        } else if (item->data[ptr + 1] == PK_BBRD){
+          if(item->vt->vbus->ctb(arg)){
+            if(walkPtr(item->data, item->vt, 1, ptr)){
+              // OSAP::debug("broadcasting on ch " + String(arg));
+              item->vt->vbus->broadcast(item->data, item->len, arg);
+            } else {
+              OSAP::error("bbrd fails for bad ptr walk");
+            }
+            stackClearSlot(item);
+          } else {
+            // failed to bbrd, returning next... 
+          }
+        } else {
+          // doesn't make any sense, we switched in on these terms... 
+          OSAP::error("absolute nonsense", MEDIUM);
+          stackClearSlot(item);
+        }
+      }
+      break;
+    case PK_LLESCAPE:
+      OSAP::error("lldebug to embedded, dumping", MINOR);
+      stackClearSlot(item);
+      break;
+    default:
+      OSAP::error("unrecognized ptr to " + item->vt->name + " " + String(PK_READKEY(item->data[ptr + 1])), MINOR);
+      stackClearSlot(item);
+      // error, delete, 
+      break;
+  } // end swiiiitch 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/loop.h b/system/firmware/lpf-modular-motion-head/src/osape/core/loop.h
new file mode 100644
index 0000000000000000000000000000000000000000..5022aa16c00da6b40864ca8f09432dab0744ad04
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/loop.h
@@ -0,0 +1,25 @@
+/*
+osap/osapLoop.h
+
+main osap op: whips data vertex-to-vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef LOOP_H_
+#define LOOP_H_ 
+
+#include "vertex.h"
+
+// we loop, 
+void osapLoop(Vertex* root);
+// we handle, 
+void osapItemHandler(stackItem* item);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/osap.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/osap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..acde43271ecb27ea482e1b1079b02d847a15fed9
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/osap.cpp
@@ -0,0 +1,111 @@
+/*
+osap/osap.cpp
+
+osap root / vertex factory
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "osap.h"
+#include "loop.h"
+#include "packets.h"
+#include "../utils/cobs.h"
+
+// stash most recents, and counts, and high water mark, 
+uint32_t OSAP::loopItemsHighWaterMark = 0;
+uint32_t errorCount = 0;
+uint32_t debugCount = 0;
+// strings...
+unsigned char latestError[VT_SLOTSIZE];
+unsigned char latestDebug[VT_SLOTSIZE];
+uint16_t latestErrorLen = 0;
+uint16_t latestDebugLen = 0;
+
+OSAP::OSAP(String _name) : Vertex("rt_" + _name){};
+
+void OSAP::loop(void){
+  // this is the root, so we kick all of the internal net operation from here 
+  osapLoop(this);
+}
+
+void OSAP::destHandler(stackItem* item, uint16_t ptr){
+  // classic switch on 'em 
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == ROOT_KEY, ptr + 3 = ID (if ack req.) 
+  uint16_t wptr = 0;
+  uint16_t len = 0;
+  switch(item->data[ptr + 2]){
+    case RT_DBG_STAT:
+    case RT_DBG_ERRMSG:
+    case RT_DBG_DBGMSG:
+      // return w/ the res key & same issuing ID 
+      payload[wptr ++] = PK_DEST;
+      payload[wptr ++] = RT_DBG_RES;
+      payload[wptr ++] = item->data[ptr + 3];
+      // stash high water mark, errormsg count, debugmsgcount 
+      ts_writeUint32(OSAP::loopItemsHighWaterMark, payload, &wptr);
+      ts_writeUint32(errorCount, payload, &wptr);
+      ts_writeUint32(debugCount, payload, &wptr);
+      // optionally, a string... I know we switch() then if(), it's uggo, 
+      if(item->data[ptr + 2] == RT_DBG_ERRMSG){
+        ts_writeString(latestError, latestErrorLen, payload, &wptr, VT_SLOTSIZE / 2);
+      } else if (item->data[ptr + 2] == RT_DBG_DBGMSG){
+        ts_writeString(latestDebug, latestDebugLen, payload, &wptr, VT_SLOTSIZE / 2);
+      }
+      // that's the payload, I figure, 
+      len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+      stackClearSlot(item);
+      stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      break;
+    default:
+      OSAP::error("unrecognized key to root node " + String(item->data[ptr + 2]));
+      stackClearSlot(item);
+      break;
+  }
+}
+
+uint8_t errBuf[255];
+uint8_t errBufEncoded[255];
+
+void debugPrint(String msg){
+  // whatever you want,
+  uint32_t len = msg.length();
+  // max this long, per the serlink bounds 
+  if(len + 9 > 255) len = 255 - 9;
+  // header... 
+  errBuf[0] = len + 8;  // len, key, cobs start + end, strlen (4) 
+  errBuf[1] = 172;      // serialLink debug key 
+  errBuf[2] = len & 255;
+  errBuf[3] = (len >> 8) & 255;
+  errBuf[4] = (len >> 16) & 255;
+  errBuf[5] = (len >> 24) & 255;
+  msg.getBytes(&(errBuf[6]), len + 1);
+  // encode from 2, leaving the len, key header... 
+  size_t ecl = cobsEncode(&(errBuf[2]), len + 4, errBufEncoded);
+  // what in god blazes ? copy back from encoded -> previous... 
+  memcpy(&(errBuf[2]), errBufEncoded, ecl);
+  // set tail to zero, to delineate, 
+  errBuf[errBuf[0] - 1] = 0;
+  // direct escape 
+  Serial.write(errBuf, errBuf[0]);
+}
+
+void OSAP::error(String msg, OSAPErrorLevels lvl){
+  //const char* str = msg.c_str();
+  msg.getBytes(latestError, VT_SLOTSIZE);
+  latestErrorLen = msg.length();
+  errorCount ++;
+  debugPrint(msg);
+}
+
+void OSAP::debug(String msg, OSAPDebugStreams stream){
+  msg.getBytes(latestDebug, VT_SLOTSIZE);
+  latestDebugLen = msg.length();
+  debugCount ++;
+  debugPrint(msg);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/osap.h b/system/firmware/lpf-modular-motion-head/src/osape/core/osap.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c2c9d789ebd23ba452c7259c3423088ff2b9f
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/osap.h
@@ -0,0 +1,38 @@
+/*
+osap/osap.h
+
+osap root / vertex factory 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_H_
+#define OSAP_H_
+
+#include "vertex.h"
+
+// largely semantic class, OSAP represents the root vertex in whichever context 
+// and it's where run the main loop from, etc... 
+// here is where we coordinate context-level stuff: adding new instances, 
+// stashing error messages & counts, etc, 
+
+enum OSAPErrorLevels { HALTING, MEDIUM, MINOR };
+enum OSAPDebugStreams { DEFAULT, LOOP };
+
+class OSAP : public Vertex {
+  public: 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr);
+    OSAP(String _name);// : Vertex(_name);
+    static void error(String msg, OSAPErrorLevels lvl = MINOR );
+    static void debug(String msg, OSAPDebugStreams stream = DEFAULT );
+    static uint32_t loopItemsHighWaterMark;
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/packets.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/packets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bf83928d99d3c173d0efdef40ab614dc2433b409
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/packets.cpp
@@ -0,0 +1,193 @@
+/*
+osap/packets.cpp
+
+common routines 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "packets.h"
+#include "ts.h"
+#include "osap.h"
+
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg){
+  buf[ptr] = key | (0b00001111 & (arg >> 8));
+  buf[ptr + 1] = arg & 0b11111111;
+}
+// not sure how I want to do this yet... 
+uint16_t readArg(uint8_t* buf, uint16_t ptr){
+  return ((buf[ptr] & 0b00001111) << 8) | buf[ptr + 1];
+}
+
+boolean findPtr(uint8_t* pck, uint16_t* pt){
+  // 1st instruction is always at pck[4], pck[0][1] == ttl, pck[2][3] == segSize 
+  uint16_t ptr = 4;
+  // there's a potential speedup where we assume given *pt is already incremented somewhat, 
+  // maybe shaves some ns... but here we just look fresh every time, 
+  for(uint8_t i = 0; i < 16; i ++){
+    switch(PK_READKEY(pck[ptr])){
+      case PK_PTR: // var is here 
+        *pt = ptr;
+        return true;
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        ptr += 2;
+        break;
+      default:
+        return false;
+    }
+  }
+  // case where no ptr after 16 hops, 
+  return false;
+}
+
+boolean walkPtr(uint8_t* pck, Vertex* source, uint8_t steps, uint16_t ptr){
+  // if the ptr we were handed isn't in the right spot, try to find it... 
+  if(pck[ptr] != PK_PTR){
+    // if that fails, bail... 
+    if(!findPtr(pck, &ptr)){
+      OSAP::error("before a ptr walk, ptr is out of place...");
+      return false;
+    }
+  }
+  // carry on w/ the walking algo, 
+  for(uint8_t s = 0; s < steps; s ++){
+    switch PK_READKEY(pck[ptr + 1]){
+      case PK_SIB:
+        {
+          // stash indice from-whence it came,
+          uint16_t txIndice = source->indice;
+          // for loop's next step, this is the source now, 
+          source = source->parent->children[readArg(pck, ptr + 1)];
+          // where ptr is currently, we stash new key/pair for a reversal, 
+          writeKeyArgPair(pck, ptr, PK_SIB, txIndice);
+          // increment packet's ptr, and our own... 
+          pck[ptr + 2] = PK_PTR; 
+          ptr += 2;
+        }
+        break;
+      case PK_PARENT:
+        // reversal for a 'parent' instruction is to bounce back down to the child, 
+        writeKeyArgPair(pck, ptr, PK_CHILD, source->indice);
+        // next source is now...
+        source = source->parent;
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      case PK_CHILD:
+        // next source is... 
+        source = source->children[readArg(pck, ptr + 1)];
+        // reversal for 'child' instruction is to go back up to parent, 
+        writeKeyArgPair(pck, ptr, PK_PARENT, 0);
+        // same increment, 
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2; 
+        break;
+      case PK_PFWD:
+        // reversal for pfwd instruction is identical, 
+        writeKeyArgPair(pck, ptr, PK_PFWD, 0);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // though this should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have port fwd w/ more than one step");
+          return false;
+        }
+        break;
+      case PK_BFWD:
+        // reversal for bfwd instruction is to return *up*... 
+        writeKeyArgPair(pck, ptr, PK_BFWD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        // this also should only ever be called w/ one step, 
+        if(steps != 1){
+          OSAP::error("likely bad call to walkPtr, we have bus fwd w/ more than one step");
+          return false; 
+        }
+        break;
+      case PK_BBRD:
+        // broadcasts are a little strange, we also stuff the ownRxAddr in,
+        writeKeyArgPair(pck, ptr, PK_BBRD, source->vbus->ownRxAddr);
+        pck[ptr + 2] = PK_PTR;
+        ptr += 2;
+        break;
+      default:
+        OSAP::error("have out of place keys in the ptr walk...");
+        return false;
+    }
+  } // end steps, alleged success,  
+  return true; 
+}
+
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen){
+  uint16_t wptr = 0;
+  ts_writeUint16(route->ttl, gram, &wptr);
+  ts_writeUint16(route->segSize, gram, &wptr);
+  memcpy(&(gram[wptr]), route->path, route->pathLen);
+  wptr += route->pathLen;
+  if(wptr + payloadLen > route->segSize){
+    OSAP::error("writeDatagram asked to write packet that exceeds segSize, bailing", MEDIUM);
+    return 0;
+  }
+  memcpy(&(gram[wptr]), payload, payloadLen);
+  wptr += payloadLen;
+  return wptr;
+}
+
+// original gram, payload, len, 
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen){
+  // 1st up, we can straight copy the 1st 4 bytes, 
+  memcpy(gram, ogGram, 4);
+  // now find a ptr, 
+  uint16_t ptr = 0;
+  if(!findPtr(ogGram, &ptr)){
+    OSAP::error("writeReply can't find the pointer...", MEDIUM);
+    return 0;
+  }
+  // do we have enough space? it's the minimum of the allowed segsize & stated maxGramLength, 
+  maxGramLength = min(maxGramLength, ts_readUint16(ogGram, 2));
+  if(ptr + 1 + payloadLen > maxGramLength){
+    OSAP::error("writeReply asked to write packet that exceeds maxGramLength, bailing", MEDIUM);
+    return 0;
+  }
+  // write the payload in, apres-pointer, 
+  memcpy(&(gram[ptr + 1]), payload, payloadLen);
+  // now we can do a little reversing... 
+  uint16_t wptr = 4;
+  uint16_t end = ptr;
+  uint16_t rptr = ptr;
+  // 1st byte... the ptr, 
+  gram[wptr ++] = PK_PTR;
+  // now for a max 16 steps, 
+  for(uint8_t h = 0; h < 16; h ++){
+    if(wptr >= end) break;
+    rptr -= 2;
+    switch(PK_READKEY(ogGram[rptr])){
+      case PK_SIB:
+      case PK_PARENT:
+      case PK_CHILD:
+      case PK_PFWD:
+      case PK_BFWD:
+      case PK_BBRD:
+        gram[wptr ++] = ogGram[rptr];
+        gram[wptr ++] = ogGram[rptr + 1];
+        break;
+      default:
+        OSAP::error("writeReply fails to reverse this packet, bailing", MEDIUM);
+        return 0;
+    }
+  } // end thru-loop, 
+  // it's written, return the len  // we had gram[ptr] = PK_PTR, so len was ptr + 1, then added payloadLen, 
+  return end + 1 + payloadLen;
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/packets.h b/system/firmware/lpf-modular-motion-head/src/osape/core/packets.h
new file mode 100644
index 0000000000000000000000000000000000000000..914656be1eb7656f481915438a10701edad23280
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/packets.h
@@ -0,0 +1,48 @@
+/*
+osap/packets.h
+
+reading / writing from osap packets / datagrams 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_PACKETS_H_
+#define OSAP_PACKETS_H_
+
+#include <Arduino.h>
+#include "vertex.h"
+
+// -------------------------------------------------------- Routing (Packet) Keys
+
+#define PK_PTR 240
+#define PK_DEST 224
+#define PK_PINGREQ 192 
+#define PK_PINGRES 176 
+#define PK_SCOPEREQ 160 
+#define PK_SCOPERES 144 
+#define PK_SIB 16 
+#define PK_PARENT 32 
+#define PK_CHILD 48 
+#define PK_PFWD 64 
+#define PK_BFWD 80
+#define PK_BBRD 96 
+#define PK_LLESCAPE 112 
+
+// to read *just the key* from key, arg pair
+#define PK_READKEY(data) (data & 0b11110000)
+
+// packet utes, 
+void writeKeyArgPair(unsigned char* buf, uint16_t ptr, uint8_t key, uint16_t arg);
+uint16_t readArg(uint8_t* buf, uint16_t ptr);
+boolean findPtr(uint8_t* pck, uint16_t* ptr);
+boolean walkPtr(uint8_t* pck, Vertex* vt, uint8_t steps, uint16_t ptr = 4);
+uint16_t writeDatagram(uint8_t* gram, uint16_t maxGramLength, Route* route, uint8_t* payload, uint16_t payloadLen);
+uint16_t writeReply(uint8_t* ogGram, uint8_t* gram, uint16_t maxGramLength, uint8_t* payload, uint16_t payloadLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/routes.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/routes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6caea0c0a00c56f339f2fdb7ec4b02278e1faf73
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/routes.cpp
@@ -0,0 +1,55 @@
+/*
+osap/routes.cpp
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "routes.h"
+#include "packets.h"
+
+Route::Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize){
+  ttl = _ttl;
+  segSize = _segSize;
+  // nope, 
+  if(_pathLen > 64){
+    _pathLen = 0;
+  }
+  memcpy(path, _path, _pathLen);
+  pathLen = _pathLen;
+}
+
+Route::Route(void){
+  path[pathLen ++] = PK_PTR;
+}
+
+Route* Route::sib(uint16_t indice){
+  writeKeyArgPair(path, pathLen, PK_SIB, indice);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::pfwd(void){
+  writeKeyArgPair(path, pathLen, PK_PFWD, 0);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bfwd(uint16_t rxAddr){
+  writeKeyArgPair(path, pathLen, PK_BFWD, rxAddr);
+  pathLen += 2;
+  return this;
+}
+
+Route* Route::bbrd(uint16_t channel){
+  writeKeyArgPair(path, pathLen, PK_BBRD, channel);
+  pathLen += 2;
+  return this; 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/routes.h b/system/firmware/lpf-modular-motion-head/src/osape/core/routes.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2bb3c97cffb7df24867de4efe7489b40daa4a0e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/routes.h
@@ -0,0 +1,38 @@
+/*
+osap/routes.h
+
+directions
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef OSAP_ROUTES_H_
+#define OSAP_ROUTES_H_
+
+#include <Arduino.h>
+
+// a route type... 
+class Route {
+  public:
+    uint8_t path[64];
+    uint16_t pathLen = 0;
+    uint16_t ttl = 1000;
+    uint16_t segSize = 128;
+    // write-direct constructor, 
+    Route(uint8_t* _path, uint16_t _pathLen, uint16_t _ttl, uint16_t _segSize);
+    // write-along constructor, 
+    Route(void);
+    // pass-thru initialize constructors, 
+    Route* sib(uint16_t indice);
+    Route* pfwd(void);
+    Route* bfwd(uint16_t rxAddr);
+    Route* bbrd(uint16_t channel);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/stack.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/stack.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..401bd7103f872bd141172d945ff2b2a8cb93e36f
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/stack.cpp
@@ -0,0 +1,138 @@
+/*
+osap/stack.cpp
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "stack.h"
+#include "vertex.h"
+#include "osap.h"
+
+// ---------------------------------------------- Stack Tools 
+
+void stackReset(Vertex* vt){
+  // clear all elements & write next ptrs in linear order 
+  for(uint8_t od = 0; od < 2; od ++){
+    // set lengths, etc, 
+    for(uint8_t s = 0; s < vt->stackSize; s ++){
+      vt->stack[od][s].arrivalTime = 0;
+      vt->stack[od][s].len = 0;
+      vt->stack[od][s].indice = s;
+      // and ptrs to self, 
+      vt->stack[od][s].vt = vt;
+      vt->stack[od][s].od = od;
+    }
+    // set next ptrs, 
+    for(uint8_t s = 0; s < vt->stackSize - 1; s ++){
+      vt->stack[od][s].next = &(vt->stack[od][s + 1]);
+    }
+    vt->stack[od][vt->stackSize - 1].next = &(vt->stack[od][0]);
+    // set previous ptrs, 
+    for(uint8_t s = 1; s < vt->stackSize; s ++){
+      vt->stack[od][s].previous = &(vt->stack[od][s - 1]);
+    }
+    vt->stack[od][0].previous = &(vt->stack[od][vt->stackSize - 1]);
+    // 1st element is 0th on startup, 
+    vt->queueStart[od] = &(vt->stack[od][0]); 
+    // first free = tail at init, 
+    vt->firstFree[od] = &(vt->stack[od][0]);
+  }
+}
+
+// -------------------------------------------------------- ORIGIN SIDE 
+// true if there's any space in the stack, 
+boolean stackEmptySlot(Vertex* vt, uint8_t od){
+  if(od > 1) return false;
+  // if 1st free has ptr to next item, not full 
+  if(vt->firstFree[od]->next->len != 0){
+    return false;
+  } else {
+    return true;
+  }
+}
+
+// loads data into stack 
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len){
+  if(od > 1) return; // bad od, lost data 
+  // copy into first free element, 
+  memcpy(vt->firstFree[od]->data, data, len);
+  vt->firstFree[od]->len = len;
+  vt->firstFree[od]->arrivalTime = millis();
+  //DEBUG("load " + String(vt->firstFree[od]->indice) + " " + String(vt->firstFree[od]->arrivalTime));
+  // now firstFree is next, 
+  vt->firstFree[od] = vt->firstFree[od]->next;
+}
+
+// -------------------------------------------------------- EXIT SIDE 
+// return count of items occupying stack, and list of ptrs to them, 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems){
+  if(od > 1) return 0;
+  // when queueStart == firstFree element, we have nothing for you 
+  if(vt->firstFree[od] == vt->queueStart[od]) return 0;
+  // starting at queue begin, 
+  uint8_t count = 0;
+  stackItem* item = vt->queueStart[od];
+  for(uint8_t s = 0; s < maxItems; s ++){
+    items[s] = item;
+    count ++;
+    if(item->next->len > 0){
+      item = item->next;
+    } else {
+      return count;
+    }
+  }
+  return count;
+}
+
+// clear the item, 
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item){
+  // this would be deadly, so:
+  if(od > 1) {
+    OSAP::error("stackClearSlot, od > 1, badness", MEDIUM);
+    return;
+  }
+  // item is 0-len, etc 
+  item->len = 0;
+  // is this
+  uint8_t indice = item->indice;
+  // if was queueStart, queueStart now at next,
+  if(vt->queueStart[od] == item){
+    vt->queueStart[od] = item->next;
+    // and wouldn't have to do any of the below? 
+  } else {
+    // pull from chain, now is free of associations, 
+    // these ops are *always two up*
+    item->previous->next = item->next;
+    item->next->previous = item->previous;
+    // now, insert this where old firstFree was 
+    vt->firstFree[od]->previous->next = item;
+    item->previous = vt->firstFree[od]->previous;    
+    item->next = vt->firstFree[od];
+    vt->firstFree[od]->previous = item;
+    // and the item is the new firstFree element, 
+    vt->firstFree[od] = item;
+  }
+  // now we callback to the vertex; these fns are often used to clear flowcontrol condns 
+  switch(od){
+    case VT_STACK_ORIGIN:
+      vt->onOriginStackClear(indice);
+      break;
+    case VT_STACK_DESTINATION:
+      vt->onDestinationStackClear(indice);
+      break;
+    default:  // guarded against this above... 
+      break;
+  }
+}
+
+void stackClearSlot(stackItem* item){
+  stackClearSlot(item->vt, item->od, item);
+}
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/stack.h b/system/firmware/lpf-modular-motion-head/src/osape/core/stack.h
new file mode 100644
index 0000000000000000000000000000000000000000..79151239b987f025150dc6f1ac580cfc4e474887
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/stack.h
@@ -0,0 +1,54 @@
+/*
+osap/stack.h
+
+graph vertex data chonk 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef STACK_H_
+#define STACK_H_ 
+
+#include <Arduino.h>
+#include "./osap_config.h" 
+
+#define VT_STACK_ORIGIN 0 
+#define VT_STACK_DESTINATION 1 
+
+class Vertex;
+
+// core routing layer chunk-of-stuff, 
+// https://stackoverflow.com/questions/1813991/c-structure-with-pointer-to-self
+typedef struct stackItem {
+  uint8_t data[VT_SLOTSIZE];          // data bytes
+  uint16_t len = 0;                   // data bytes count 
+  uint32_t arrivalTime = 0;           // ms-since-system-alive, time at last ingest
+  int32_t timeToDeath = 0;            // ms of time until pckt vanishes on this hop
+  Vertex* vt;                         // vertex to whomst we belong, 
+  uint8_t od;                         // origin / destination to which we belong, 
+  uint8_t indice;                     // actual physical position in the stack 
+  uint16_t ptr = 0;                   // current data[ptr] == 88 
+  stackItem* next = nullptr;          // linked ringbuffer next 
+  stackItem* previous = nullptr;      // linked ringbuffer previous 
+} stackItem;
+
+// stack setup / reset 
+void stackReset(Vertex* vt);
+
+// stack origin side 
+boolean stackEmptySlot(Vertex* vt, uint8_t od);
+void stackLoadSlot(Vertex* vt, uint8_t od, uint8_t* data, uint16_t len);
+
+// stack exit side 
+uint8_t stackGetItems(Vertex* vt, uint8_t od, stackItem** items, uint8_t maxItems);
+void stackClearSlot(Vertex* vt, uint8_t od, stackItem* item);
+void stackClearSlot(stackItem* item);
+
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/ts.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/ts.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3cd3fdc9c1c249d25b22b75fa9fc69f311d04c19
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/ts.cpp
@@ -0,0 +1,183 @@
+/*
+osap/ts.cpp
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "ts.h"
+
+// ---------------------------------------------- Reading 
+
+// boolean 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr){
+  if(buf[(*ptr) ++]){
+    *val = true;
+  } else {
+    *val = false;
+  }
+}
+
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr){
+  boolean val = buf[(*ptr)] ? true : false;
+  (*ptr) += 1;
+  return val;
+}
+
+// uint8 
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr){
+  uint8_t val = buf[(*ptr)];
+  (*ptr) += 1;
+  return val;
+}
+
+// uint16 
+
+void ts_readUint16(uint16_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 2;
+}
+
+#warning some of these are pretty vague, i.e. this ingests a pointer *not as a pointer* (lol)
+// so it doesn't increment it, whereas the readUint8 above *does so* - ... ?? pick a style ? 
+uint16_t ts_readUint16(unsigned char* buf, uint16_t ptr){
+  return (buf[ptr + 1] << 8) | buf[ptr];
+}
+
+// uint32 
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr){
+  *val = buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)];
+  *ptr += 4;
+}
+
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr){
+  uint32_t val = (buf[(*ptr) + 3] << 24 | buf[(*ptr) + 2] << 16 | buf[(*ptr) + 1] << 8 | buf[(*ptr)]);
+  (*ptr) += 4;
+  return val;
+}
+
+// int32 
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.i;
+}
+
+// float32 
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk = { .bytes = { buf[(*ptr)], buf[(*ptr) + 1], buf[(*ptr) + 2], buf[(*ptr) + 3] } };
+  (*ptr) += 4;
+  return chunk.f;
+}
+
+// -------------------------------------------------------- Writing 
+
+// boolean
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr){
+  if(val){
+    buf[(*ptr) ++] = 1;
+  } else {
+    buf[(*ptr) ++] = 0;
+  }
+}
+
+// unsigned 
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val;
+}
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+}
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr){
+  buf[(*ptr) ++] = val & 255;
+  buf[(*ptr) ++] = (val >> 8) & 255;
+  buf[(*ptr) ++] = (val >> 16) & 255;
+  buf[(*ptr) ++] = (val >> 24) & 255;
+}
+
+// signed 
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int16 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+}
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr){
+  chunk_int32 chunk = { i: val };
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+// floats 
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float32 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+}
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr){
+  chunk_float64 chunk;
+  chunk.f = val;
+  buf[(*ptr) ++] = chunk.bytes[0];
+  buf[(*ptr) ++] = chunk.bytes[1];
+  buf[(*ptr) ++] = chunk.bytes[2];
+  buf[(*ptr) ++] = chunk.bytes[3];
+  buf[(*ptr) ++] = chunk.bytes[4];
+  buf[(*ptr) ++] = chunk.bytes[5];
+  buf[(*ptr) ++] = chunk.bytes[6];
+  buf[(*ptr) ++] = chunk.bytes[7];
+}
+
+// string, overloaded ?
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr){
+  uint32_t len = val->length();
+  buf[(*ptr) ++] = len & 255;
+  buf[(*ptr) ++] = (len >> 8) & 255;
+  buf[(*ptr) ++] = (len >> 16) & 255;
+  buf[(*ptr) ++] = (len >> 24) & 255;
+  val->getBytes(&buf[*ptr], len + 1);
+  *ptr += len;
+}
+
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen){
+  if(strLen > maxLen) strLen = maxLen;
+  buf[(*ptr) ++] = strLen & 255;
+  buf[(*ptr) ++] = (strLen >> 8) & 255;
+  buf[(*ptr) ++] = (strLen >> 16) & 255;
+  buf[(*ptr) ++] = (strLen >> 24) & 255;
+  // write in one-by-one, surely there is a better way, 
+  for(uint16_t i = 0; i < strLen; i ++){
+    buf[(*ptr) ++] = str[i];
+  }
+  *ptr += strLen;
+}
+
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr){
+  ts_writeString(&val, buf, ptr);
+}
+
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/ts.h b/system/firmware/lpf-modular-motion-head/src/osape/core/ts.h
new file mode 100644
index 0000000000000000000000000000000000000000..63e77b2b02c0f7716bb1cba55cc9eef613d1207f
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/ts.h
@@ -0,0 +1,157 @@
+/*
+core/ts.h
+
+typeset / keys / writing / reading
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef TS_H_
+#define TS_H_
+
+#include <Arduino.h>
+
+// -------------------------------------------------------- Vertex Type Keys
+// will likely use these in the netrunner: 
+
+#define VT_TYPE_ROOT 22       // top level 
+#define VT_TYPE_MODULE 23     // collection of things (?) or something, idk yet 
+#define VT_TYPE_ENDPOINT 24   // software endpoint w/ read/write semantics 
+#define VT_TYPE_QUERY 25 
+#define VT_TYPE_ENDPOINT_MULTISEG 26 // likewise, but requring multisegment transmission 
+#define VT_TYPE_CODE 25       // autonomous graph dwellers 
+#define VT_TYPE_VPORT 44      // virtual ports 
+#define VT_TYPE_VBUS 45       // maybe bus-drop / bus-head / bus-cohost are differentiated 
+
+// -------------------------------------------------------- Endpoint Keys 
+
+#define EP_SS_ACK 101       // the ack 
+#define EP_SS_ACKLESS 121   // single segment, no ack 
+#define EP_SS_ACKED 122     // single segment, request ack 
+#define EP_QUERY 131        // query request 
+#define EP_QUERY_RESP 132   // reply to query request 
+#define EP_ROUTE_QUERY_REQ 141 
+#define EP_ROUTE_QUERY_RES 142
+#define EP_ROUTE_SET_REQ 143
+#define EP_ROUTE_SET_RES 144 
+#define EP_ROUTE_RM_REQ 147
+#define EP_ROUTE_RM_RES 148 
+
+#define EP_ROUTEMODE_ACKED 167
+#define EP_ROUTEMODE_ACKLESS 168 
+
+// -------------------------------------------------------- Root Keys 
+
+#define RT_DBG_STAT 151
+#define RT_DBG_ERRMSG 152 
+#define RT_DBG_DBGMSG 153
+#define RT_DBG_RES 161
+
+// -------------------------------------------------------- VBus MVC Keys 
+
+#define VBUS_BROADCAST_MAP_REQ 145
+#define VBUS_BROADCAST_MAP_RES 146
+#define VBUS_BROADCAST_QUERY_REQ 141
+#define VBUS_BROADCAST_QUERY_RES 142
+#define VBUS_BROADCAST_SET_REQ 143
+#define VBUS_BROADCAST_SET_RES 144 
+#define VBUS_BROADCAST_RM_REQ 147 
+#define VBUS_BROADCAST_RM_RES 148 
+
+// -------------------------------------------------------- BUS ACTION KEYS (outside OSAP scope)
+
+#define UB_AK_SETPOS 102
+#define UB_AK_GOTOPOS 105 
+
+// -------------------------------------------------------- Type Keys 
+
+#define TK_BOOL     2
+
+#define TK_UINT8    4
+#define TK_INT8     5
+#define TK_UINT16   6
+#define TK_INT16    7
+#define TK_UINT32   8
+#define TK_INT32    9
+#define TK_UINT64   10
+#define TK_INT64    11
+
+#define TK_FLOAT16  24
+#define TK_FLOAT32  26
+#define TK_FLOAT64  28
+
+// -------------------------------------------------------- Chunks
+
+union chunk_float32 {
+  uint8_t bytes[4];
+  float f;
+};
+
+union chunk_float64 {
+  uint8_t bytes[8];
+  double f;
+};
+
+union chunk_int16 {
+  uint8_t bytes[2];
+  int16_t i;
+};
+
+union chunk_int32 {
+  uint8_t bytes[4];
+  int32_t i;
+};
+
+union chunk_uint32 {
+    uint8_t bytes[4];
+    uint32_t u;
+}; 
+
+// -------------------------------------------------------- Reading 
+
+void ts_readBoolean(boolean* val, unsigned char* buf, uint16_t* ptr);
+boolean ts_readBoolean(unsigned char* buf, uint16_t* ptr);
+
+uint8_t ts_readUint8(unsigned char* buf, uint16_t* ptr);
+
+void ts_readUint16(uint16_t* val, uint8_t* buf, uint16_t* ptr);
+uint16_t ts_readUint16(uint8_t* buf, uint16_t ptr);
+
+void ts_readUint32(uint32_t* val, unsigned char* buf, uint16_t* ptr);
+uint32_t ts_readUint32(unsigned char* buf, uint16_t* ptr);
+
+int32_t ts_readInt32(unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+// -------------------------------------------------------- Writing 
+
+void ts_writeBoolean(boolean val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint8(uint8_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint16(uint16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeUint32(uint32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt16(int16_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeInt32(int32_t val, unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat32(float val, volatile unsigned char* buf, uint16_t* ptr);
+
+float ts_readFloat32(unsigned char* buf, uint16_t* ptr);
+
+void ts_writeFloat64(double val, volatile unsigned char* buf, uint16_t* ptr);
+
+void ts_writeString(String* val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(String val, unsigned char* buf, uint16_t* ptr);
+void ts_writeString(unsigned char* str, uint16_t strLen, unsigned char* buf, uint16_t* ptr, uint16_t maxLen);
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.cpp b/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9ce012af681a42b059c6585888d1db806dd2ab51
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.cpp
@@ -0,0 +1,327 @@
+/*
+osap/vertex.cpp
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vertex.h"
+#include "stack.h"
+#include "osap.h"
+#include "packets.h"
+
+// ---------------------------------------------- Temporary Stash 
+
+uint8_t Vertex::payload[VT_SLOTSIZE];
+uint8_t Vertex::datagram[VT_SLOTSIZE];
+
+// ---------------------------------------------- Vertex Constructor and Defaults 
+
+Vertex::Vertex( 
+  Vertex* _parent, String _name, 
+  void (*_loop)(Vertex* vt),
+  void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+  void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+){
+  // name self, reset stack... 
+  name = _name;
+  stackReset(this);
+  // callback assignments... 
+  loop_cb = _loop;
+  onOriginStackClear_cb = _onOriginStackClear;
+  onDestinationStackClear_cb = _onDestinationStackClear;
+  // insert self to osap net,
+  if(_parent == nullptr){
+    type = VT_TYPE_ROOT;
+    indice = 0;
+  } else {
+    if (_parent->numChildren >= VT_MAXCHILDREN) {
+      OSAP::error("trying to nest a vertex under " + _parent->name + " but we have reached VT_MAXCHILDREN limit", HALTING);
+    } else {
+      this->indice = _parent->numChildren;
+      this->parent = _parent;
+      _parent->children[_parent->numChildren ++] = this;
+    }
+  }
+}
+
+void Vertex::loop(void){
+  if(loop_cb != nullptr) return loop_cb(this);
+}
+
+void Vertex::destHandler(stackItem* item, uint16_t ptr){
+  // generic handler...
+  OSAP::debug("generic destHandler at " + name);
+  stackClearSlot(item);
+}
+
+void Vertex::pingRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_PINGRES;
+  payload[1] = item->data[ptr + 2];
+  // write a new gram, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 2);
+  // clear previous, 
+  stackClearSlot(item);
+  // load next... there will be one empty, as this has just arrived here... & we just wiped it 
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+void Vertex::scopeRequestHandler(stackItem* item, uint16_t ptr){
+  // key & id, 
+  payload[0] = PK_SCOPERES;
+  payload[1] = item->data[ptr + 2];
+  // next items write starting here, 
+  uint16_t wptr = 2;
+  // scope time-tag, 
+  ts_writeUint32(scopeTimeTag, payload, &wptr);
+  // and read in the previous scope (this is traversal state required to delineate loops in the graph) 
+  uint16_t rptr = ptr + 3;
+  ts_readUint32(&scopeTimeTag, item->data, &rptr);
+  // write the vertex type,  
+  payload[wptr ++] = type;
+  // vport / vbus link states, 
+  if(type == VT_TYPE_VPORT){
+    payload[wptr ++] = (vport->isOpen() ? 1 : 0);
+  } else if (type == VT_TYPE_VBUS){
+    uint16_t addrSize = vbus->addrSpaceSize;
+    uint16_t addr = 0;
+    // ok we write the address size in first, then our own rxaddr, 
+    ts_writeUint16(vbus->addrSpaceSize, payload, &wptr);
+    ts_writeUint16(vbus->ownRxAddr, payload, &wptr);
+    // then *so long a we're not overwriting*, we stuff link-state bytes, 
+    while(wptr + 8 + name.length() <= VT_SLOTSIZE){
+      payload[wptr] = 0;
+      for(uint8_t b = 0; b < 8; b ++){
+        payload[wptr] |= (vbus->isOpen(addr) ? 1 : 0) << b;
+        addr ++;
+        if(addr >= addrSize) goto end;
+      }
+      wptr ++;
+    }
+    end:
+    wptr ++; // += 1 more, so we write into next, 
+  }
+  // our own indice, # siblings, and # children, 
+  ts_writeUint16(indice, payload, &wptr);
+  if(parent != nullptr){
+    ts_writeUint16(parent->numChildren, payload, &wptr);
+  } else {
+    ts_writeUint16(0, payload, &wptr);
+  }
+  ts_writeUint16(numChildren, payload, &wptr);
+  // finally, our string name:
+  ts_writeString(name, payload, &wptr);
+  // and roll that back up, rm old, and ship it, 
+  uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+  stackClearSlot(item);
+  stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+}
+
+
+void Vertex::onOriginStackClear(uint8_t slot){
+  if(onOriginStackClear_cb != nullptr) return onOriginStackClear_cb(this, slot);
+}
+
+void Vertex::onDestinationStackClear(uint8_t slot){
+  if(onDestinationStackClear_cb != nullptr) return onDestinationStackClear_cb(this, slot);
+}
+
+// ---------------------------------------------- VPort Constructor and Defaults 
+
+VPort::VPort(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vp_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VPORT;
+  vport = this; 
+}
+
+// ---------------------------------------------- VBus Constructor and Defaults 
+
+VBus::VBus(
+  Vertex* _parent, String _name
+) : Vertex(_parent, "vb_" + _name, nullptr, nullptr, nullptr) {
+  // set type, reacharound, & callbacks 
+  type = VT_TYPE_VBUS;
+  vbus = this;
+  // these should all init to nullptr, 
+  for(uint8_t ch = 0; ch < VBUS_MAX_BROADCAST_CHANNELS; ch ++){
+    broadcastChannels[ch] = nullptr;
+  }
+}
+
+void VBus::injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  // ok so first we want to see if we have anything sub'd to this channel, so
+  if(broadcastChannels[broadcastChannel] != nullptr){
+    // we have a route, so we want to load this data *as we inject some new path segments* 
+    Route* route = broadcastChannels[broadcastChannel];
+    // we could definitely do this faster w/o using the stackLoadSlot fn, but we won't do that yet... 
+    // will use the vertex-global datagram stash for that 
+    uint16_t ptr = 0; 
+    if(!findPtr(data, &ptr)){ OSAP::error("can't find ptr during broadcast injest", MEDIUM); return; }
+    // packet should look like 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <payload>
+    // we want to inject the channel's route such that 
+    // ttl, segsize, <prev_instruct>, <bbrd_txAddr>, PTR, <ch_route>, <payload>
+    // shouldn't actually be too difficult, eh?
+    // we do need to guard on lengths, 
+    if(len + route->pathLen > VT_SLOTSIZE){ OSAP::error("datagram + channel route is too large", MEDIUM); return; }
+    // copy up to PTR: pck[ptr] == PK_PTR, so we want to *include* this byte, having len ptr + 1, 
+    memcpy(datagram, data, ptr + 1);
+    // copy in route, but recall that as initialized, route->path[0] == PK_PTR, we don't want to double that up, 
+    memcpy(&(datagram[ptr + 1]), &(route->path[1]), route->pathLen - 1);
+    // then the rest of the gram, from just after-the-ptr, to end, 
+    memcpy(&datagram[ptr + 1 + route->pathLen - 1], &(data[ptr + 1]), len - ptr - 1);
+    // now we can load this in, 
+    stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len + route->pathLen - 1);
+    // aye that's it innit? 
+  }
+}
+
+void VBus::setBroadcastChannel(uint8_t channel, Route* route){
+  if(channel >= VBUS_MAX_BROADCAST_CHANNELS) return;
+  // seems a little sus, idk 
+  broadcastChannels[channel] = route;
+}
+
+void VBus::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == the key we're switching on...
+  switch(item->data[ptr + 2]){
+    case VBUS_BROADCAST_MAP_REQ:
+      // mvc request a map of our active broadcast channels, this is akin to bus link-state-scope packet
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_MAP_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // max length of channels... max 255, same as max endpoint routes (?) 
+        // this is maybe an error, consult packet spec (transport layer) for completeness, 
+        // time being... rare to have > 255 broadcast channels, 
+        payload[wptr ++] = VBUS_MAX_BROADCAST_CHANNELS;
+        // then *so long a we're not overwriting*, we stuff link-state bytes, 
+        // idk, 32 is arbitrary, we have to account for return-route length properly... 
+        uint16_t channel = 0;
+        while(wptr + 32 <= VT_SLOTSIZE){
+          payload[wptr] = 0;
+          for(uint8_t b = 0; b < 8; b ++){
+            payload[wptr] |= (broadcastChannels[channel] == nullptr ? 0 : 1) << b;
+            channel ++;
+            if(channel >= VBUS_MAX_BROADCAST_CHANNELS) goto end;
+          }
+          wptr ++;
+        }
+        end:
+        wptr ++; // += 1 more, so we write into next, 
+        // we're ready to write the reply back, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_QUERY_REQ:
+      // mvc requests broadcast channel info on a particular channel, 
+      {
+        uint16_t wptr = 0;
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_QUERY_RES;
+        payload[wptr ++] = item->data[ptr + 3];
+        // the indice of the channel we're looking at, 
+        uint16_t ch = item->data[ptr + 4];
+        // if the ch exists, 
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS && broadcastChannels[ch] != nullptr){
+          payload[wptr ++] = 1;
+          // now... these are route objects, but we only use the path part... 
+          // but we'll re-use route-object serialization schemes from EP_ROUTE_QUERY_REQ 
+          ts_writeUint16(broadcastChannels[ch]->ttl, payload, &wptr);
+          ts_writeUint16(broadcastChannels[ch]->segSize, payload, &wptr);
+          // path copy 
+          memcpy(&(payload[wptr]), broadcastChannels[ch]->path, broadcastChannels[ch]->pathLen);
+          wptr += broadcastChannels[ch]->pathLen;
+        } else {
+          payload[wptr ++] = 0;
+        }
+        // write reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_SET_REQ:
+      // mvc requests to set a broadcast channel route 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // ch to write into...
+        uint8_t ch = item->data[ptr + 4];
+        // reply-write-pointer 
+        uint16_t wptr = 0;
+        // prep a response, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_SET_RES;
+        payload[wptr ++] = id;
+        if(ch >= VBUS_MAX_BROADCAST_CHANNELS){
+          // won't go 
+          OSAP::error("attempt to write to oob broadcast channel");
+          payload[wptr ++] = 0;
+        } else {
+          // should go 
+          payload[wptr ++] = 1;          
+          if(broadcastChannels[ch] != nullptr) OSAP::debug("overwriting previous broadcast ch at " + String(ch));
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          setBroadcastChannel(ch, new Route(path, pathLen, ttl, segSize));
+        }
+        // in any case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    case VBUS_BROADCAST_RM_REQ:
+      // mvc requests to rm a broadcast channel, 
+      // todo / cleanliness: might be salient to 'write 0' to delete (?) who knows 
+      {
+        // id & indice to rm 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t ch = item->data[ptr + 4];
+        uint16_t wptr = 0;
+        // prep res 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = VBUS_BROADCAST_RM_RES;
+        payload[wptr ++] = id;
+        // can we rm ?
+        if(ch < VBUS_MAX_BROADCAST_CHANNELS){
+          if(broadcastChannels[ch] != nullptr) {
+            delete broadcastChannels[ch];
+            broadcastChannels[ch] = nullptr;
+            payload[wptr ++] = 1;
+          } else {
+            // didn't exist, so, a bad delete: 
+            payload[wptr ++] = 0;
+          }
+        } else {
+          // bad req, should throw errors... 
+          payload[wptr ++] = 0;
+        }
+        // can send now, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+        break;
+      }
+    default:
+      OSAP::error("vbus rx msg w/ unrecognized vbus key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.h b/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.h
new file mode 100644
index 0000000000000000000000000000000000000000..842d5733f64fa6661165c84e2193b2c0604892d1
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/core/vertex.h
@@ -0,0 +1,131 @@
+/*
+osap/vertex.h
+
+graph vertex 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VERTEX_H_
+#define VERTEX_H_
+
+#include <Arduino.h> 
+#include "ts.h"
+#include "routes.h"
+#include "stack.h"
+// vertex config is build dependent, define in <folder-containing-osape>/osapConfig.h 
+#include "./osap_config.h" 
+
+// we have the vertex type, 
+// since it contains ptrs to others of its type, we fwd declare the type...
+class Vertex;
+// ... 
+typedef struct stackItem stackItem;
+typedef struct VPort VPort;
+typedef struct VBus VBus;
+
+// default vt fns 
+void vtLoopDefault(Vertex* vt);
+void vtOnOriginStackClearDefault(Vertex* vt, uint8_t slot);
+void vtOnDestinationStackClearDefault(Vertex* vt, uint8_t slot);
+
+// addressable node in the graph ! 
+class Vertex {
+  public:
+    // just temporary stashes, used all over the place to prep messages... 
+    static uint8_t payload[VT_SLOTSIZE];
+    static uint8_t datagram[VT_SLOTSIZE];
+    // -------------------------------- FN PTRS 
+    // these are *genuine function ptrs* not member functions, my dudes 
+    void (*loop_cb)(Vertex* vt) = nullptr;
+    // to notify for clear-out callbacks / flowcontrol etc 
+    void (*onOriginStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    void (*onDestinationStackClear_cb)(Vertex* vt, uint8_t slot) = nullptr;
+    // -------------------------------- Methods
+    virtual void loop(void);
+    virtual void destHandler(stackItem* item, uint16_t ptr);
+    void pingRequestHandler(stackItem* item, uint16_t ptr);
+    void scopeRequestHandler(stackItem* item, uint16_t ptr);
+    virtual void onOriginStackClear(uint8_t slot);
+    virtual void onDestinationStackClear(uint8_t slot);
+    // -------------------------------- DATA
+    // a type, a position, a name 
+    uint8_t type = VT_TYPE_CODE;
+    uint16_t indice = 0;
+    String name; 
+    // a time tag, for when we were last scoped (need for graph traversals, final implementation tbd)
+    uint32_t scopeTimeTag = 0;
+    // stacks; 
+    // origin stack[0] destination stack[1]
+    // destination stack is for messages delivered to this vertex, 
+    stackItem stack[2][VT_STACKSIZE];
+    uint8_t stackSize = VT_STACKSIZE; // should be variable 
+    //uint8_t lastStackHandled[2] = { 0, 0 };
+    stackItem* queueStart[2] = { nullptr, nullptr };    // data is read from the tail  
+    stackItem* firstFree[2] = { nullptr, nullptr };     // data is loaded into the head 
+    // parent & children (other vertices)
+    Vertex* parent = nullptr;
+    Vertex* children[VT_MAXCHILDREN]; // I think this is OK on storage: just pointers 
+    uint16_t numChildren = 0;
+    // sometimes a vertex is a vport, sometimes it is a vbus, 
+    VPort* vport;
+    VBus* vbus;
+    // -------------------------------- CONSTRUCTORS 
+    Vertex( 
+      Vertex* _parent, 
+      String _name, 
+      void (*_loop)(Vertex* vt),
+      void (*_onOriginStackClear)(Vertex* vt, uint8_t slot),
+      void (*_onDestinationStackClear)(Vertex* vt, uint8_t slot)
+    );
+    Vertex(Vertex* _parent, String _name) : Vertex(_parent, _name, nullptr, nullptr, nullptr){};
+    Vertex(String _name) : Vertex(nullptr, _name, nullptr, nullptr, nullptr){};
+};
+
+// ---------------------------------------------- VPort 
+
+class VPort : public Vertex {
+  public:
+    // -------------------------------- OK these bbs are methods, 
+    virtual void send(uint8_t* data, uint16_t len) = 0;
+    virtual boolean cts(void) = 0;
+    virtual boolean isOpen(void) = 0;
+    // base constructor, 
+    VPort(Vertex* _parent, String _name);
+};
+
+// ---------------------------------------------- VBus 
+
+class VBus : public Vertex{
+  public:
+    // -------------------------------- Methods: these are purely virtual... 
+    virtual void send(uint8_t* data, uint16_t len, uint8_t rxAddr) = 0;
+    virtual void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) = 0;
+    // clear to send, clear to broadcast, 
+    virtual boolean cts(uint8_t rxAddr) = 0;
+    virtual boolean ctb(uint8_t broadcastChannel) = 0;
+    // link state per rx-addr,
+    virtual boolean isOpen(uint8_t rxAddr) = 0;
+    // handle things aimed at us, for mvc etc 
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // busses can read-in to broadcasts,
+    void injestBroadcastPacket(uint8_t* data, uint16_t len, uint8_t broadcastChannel);
+    // we have also... broadcast channels... these are little route stubs & channel pairs, which we just straight up index, 
+    Route* broadcastChannels[VBUS_MAX_BROADCAST_CHANNELS];
+    // have to update those... 
+    void setBroadcastChannel(uint8_t channel, Route* route);
+    // has an rx addr, 
+    uint16_t ownRxAddr = 0;
+    // has a width-of-addr-space, 
+    uint16_t addrSpaceSize = 0;
+    // base constructor, children inherit... 
+    VBus(Vertex* _parent, String _name);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.cpp b/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..81cc05bb3b38d85273a838a4b05df31bff2783a9
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.cpp
@@ -0,0 +1,70 @@
+/*
+utils/cobs.cpp
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "cobs.h"
+// str8 crib from
+// https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+/** COBS encode data to buffer
+	@param data Pointer to input data to encode
+	@param length Number of bytes to encode
+	@param buffer Pointer to encoded output buffer
+	@return Encoded buffer length in bytes
+	@note doesn't write stop delimiter 
+*/
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer){
+
+	uint8_t *encode = buffer; // Encoded byte pointer
+	uint8_t *codep = encode++; // Output code pointer
+	uint8_t code = 1; // Code value
+
+	for (const uint8_t *byte = (const uint8_t *)data; length--; ++byte){
+		if (*byte) // Byte not zero, write it
+			*encode++ = *byte, ++code;
+
+		if (!*byte || code == 0xff){ // Input is zero or block completed, restart
+			*codep = code, code = 1, codep = encode;
+			if (!*byte || length)
+				++encode;
+		}
+	}
+	*codep = code;  // Write final code value
+	return encode - buffer;
+}
+
+/** COBS decode data from buffer
+	@param buffer Pointer to encoded input bytes
+	@param length Number of bytes to decode
+	@param data Pointer to decoded output data
+	@return Number of bytes successfully decoded
+	@note Stops decoding if delimiter byte is found
+*/
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data){
+
+	const uint8_t *byte = buffer; // Encoded input byte pointer
+	uint8_t *decode = (uint8_t *)data; // Decoded output byte pointer
+
+	for (uint8_t code = 0xff, block = 0; byte < buffer + length; --block){
+		if (block) // Decode block byte
+			*decode++ = *byte++;
+		else
+		{
+			if (code != 0xff) // Encoded zero, write it
+				*decode++ = 0;
+			block = code = *byte++; // Next block length
+			if (code == 0x00) // Delimiter code found
+				break;
+		}
+	}
+
+	return decode - (uint8_t *)data;
+}
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.h b/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.h
new file mode 100644
index 0000000000000000000000000000000000000000..b47070ca26d021f113da680a6835df65712d4007
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/utils/cobs.h
@@ -0,0 +1,24 @@
+/*
+utils/cobs.h
+
+consistent overhead byte stuffing implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UTIL_COBS_H_
+#define UTIL_COBS_H_
+
+#include <Arduino.h>
+
+size_t cobsEncode(const void *data, size_t length, uint8_t *buffer);
+
+size_t cobsDecode(const uint8_t *buffer, size_t length, void *data);
+
+#endif
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.cpp b/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e5d9fe310be794e69ef9040e2ee33a26bcf986f9
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.cpp
@@ -0,0 +1,351 @@
+/*
+osape/vertices/endpoint.cpp
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "endpoint.h"
+#include "../core/osap.h"
+#include "../core/packets.h"
+
+// -------------------------------------------------------- Constructors 
+
+// route constructor 
+EndpointRoute::EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+  if(_mode != EP_ROUTEMODE_ACKED && _mode != EP_ROUTEMODE_ACKLESS){
+    _mode = EP_ROUTEMODE_ACKLESS;
+  }
+  route = _route;
+  ackMode = _mode;
+  timeoutLength = _timeoutLength;
+}
+
+EndpointRoute::~EndpointRoute(void){
+  delete route;
+}
+
+// base constructor, 
+Endpoint::Endpoint(
+  Vertex* _parent, String _name, 
+  EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+  boolean (*_beforeQuery)(void)
+) : Vertex(_parent, "ep_" + _name) {
+  // type, 
+	type = VT_TYPE_ENDPOINT;
+  // set callbacks,
+  if(_onData) onData_cb = _onData;
+  if(_beforeQuery) beforeQuery_cb = _beforeQuery;
+}
+
+// -------------------------------------------------------- Dummies / Defaults 
+
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len){
+  return EP_ONDATA_ACCEPT;
+}
+
+boolean beforeQueryDefault(void){
+  return true;
+}
+
+// -------------------------------------------------------- Endpoint Route / Write API 
+
+void Endpoint::write(uint8_t* _data, uint16_t len){
+  // copy data in,
+  if(len > VT_SLOTSIZE) return; // no lol 
+  memcpy(data, _data, len);
+  dataLen = len;
+  // set route freshness 
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state == EP_TX_AWAITING_ACK){
+      routes[r]->state = EP_TX_AWAITING_AND_FRESH;
+    } else {
+      routes[r]->state = EP_TX_FRESH;
+    }
+  }
+}
+
+// add a route to an endpoint, returns indice where it's dropped, 
+uint8_t Endpoint::addRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength){
+	// guard against more-than-allowed routes 
+	if(numRoutes >= ENDPOINT_MAX_ROUTES) {
+    OSAP::error("route add is oob", MEDIUM); 
+    return 0;
+	}
+  // build, stash, increment 
+  uint8_t indice = numRoutes;
+  routes[numRoutes ++] = new EndpointRoute(_route, _mode, _timeoutLength);
+  return indice; 
+}
+
+boolean Endpoint::clearToWrite(void){
+  for(uint8_t r = 0; r < numRoutes; r ++){
+    if(routes[r]->state != EP_TX_IDLE){
+      return false;
+    }
+  }
+  return true;
+}
+
+// -------------------------------------------------------- Loop 
+
+void Endpoint::loop(void){
+  // ok we are doing a time-based dispatch... 
+  unsigned long now = millis();
+  EndpointRoute* routeTxList[ENDPOINT_MAX_ROUTES];
+  uint8_t numTxRoutes = 0;
+  // stack fresh routes, and also transition timeouts / etc, 
+  // we make & sort this list, but set it up round-robin, since many 
+  // cases will see the same TTL & same write-to time, meaning routes that 
+  // happen to be in low indices would chance on "higher priority" 
+  uint8_t r = lastRouteServiced;
+  for(uint8_t i = 0; i < numRoutes; i ++){
+    r ++; if(r >= numRoutes) r = 0;
+    switch(routes[r]->state){
+      case EP_TX_FRESH:
+        routeTxList[numTxRoutes ++] = routes[r];
+        break;
+      case EP_TX_AWAITING_ACK:
+				// check timeout & transition to idle state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_IDLE;
+        }
+				break;
+      case EP_TX_AWAITING_AND_FRESH:
+        // check timeout & transition to fresh state 
+        if(routes[r]->lastTxTime + routes[r]->timeoutLength > now){
+          routes[r]->state = EP_TX_FRESH;
+        }
+      default:
+        // noop for IDLE / otherwise...
+        break;
+    }
+  }
+  // now, would do a sort... they're all fresh at the same time, so lowest TTL would win,
+  // this one we would want to be stable, meaning original order is preserved in 
+  // otherwise identical cases, since we round-robin fairness as well as TTL / TTD  
+  #warning no sort algo yet, 
+  // serve 'em... these are all EP_TX_FRESH state, 
+  for(r = 0; r < numTxRoutes; r ++){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // make sure we'll have enough space...
+      if(dataLen + routeTxList[r]->route->pathLen + 3 >= VT_SLOTSIZE){
+        OSAP::error("attempting to write oversized datagram at " + name, MEDIUM);
+        routeTxList[r]->state = EP_TX_IDLE;
+        continue;
+      }
+      // write dest key, mode key, & id if acked, 
+      uint16_t wptr = 0;
+      payload[wptr ++] = PK_DEST;
+      if(routeTxList[r]->ackMode == EP_ROUTEMODE_ACKLESS){
+        payload[wptr ++] = EP_SS_ACKLESS;
+      } else {
+        payload[wptr ++] = EP_SS_ACKED;
+        payload[wptr ++] = nextAckID;
+        routeTxList[r]->ackId = nextAckID;
+        nextAckID ++;
+      } 
+      // write data into the payload, 
+      memcpy(&(payload[wptr]), data, dataLen);
+      wptr += dataLen;
+      // write the packet, 
+      uint16_t len = writeDatagram(datagram, VT_SLOTSIZE, routeTxList[r]->route, payload, wptr);
+      // tx time is now, and state is awaiting ack, 
+      routeTxList[r]->lastTxTime = now;
+      routeTxList[r]->state = EP_TX_AWAITING_ACK;
+      lastRouteServiced = r;
+      // ingest it...
+      stackLoadSlot(this, VT_STACK_ORIGIN, datagram, len);
+    } else {
+      // stack has no more empty slots, bail from the loop, 
+      break;
+    }
+  } // end fresh-tx-awaiting state checks, 
+}
+
+// -------------------------------------------------------- Destination Handler  
+
+void Endpoint::destHandler(stackItem* item, uint16_t ptr){
+  // item->data[ptr] == PK_PTR, ptr + 1 == PK_DEST, ptr + 2 == EP_KEY, ptr + 3 = ID (if ack req.) 
+  switch(item->data[ptr + 2]){
+    case EP_SS_ACKLESS:
+      { // singlesegment transmit-to-us, w/o ack, 
+        uint8_t* rxData = &(item->data[ptr + 3]); uint16_t rxLen = item->len - (ptr + 4);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+        switch(resp){
+          case EP_ONDATA_WAIT:    // in a wait case, we no-op / escape, it comes back around 
+            item->arrivalTime = millis();
+            break;
+          case EP_ONDATA_ACCEPT:  // here we copy it in, but carry on to the reject term to delete og gram
+            memcpy(data, rxData, rxLen);
+            dataLen = rxLen;
+          case EP_ONDATA_REJECT:  // here we simply reject it, 
+            stackClearSlot(item);
+            break;
+        } // end resp-handler, 
+      }
+      break;
+    case EP_SS_ACKED:
+      { // singlesegment transmit-to-us, w/ ack, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t* rxData = &(item->data[ptr + 4]); uint16_t rxLen = item->len - (ptr + 5);
+        EP_ONDATA_RESPONSES resp = onData_cb(rxData, rxLen);
+          switch(resp){
+            case EP_ONDATA_WAIT: // this is a little danger-danger, 
+              item->arrivalTime = millis();
+              break;
+            case EP_ONDATA_ACCEPT:
+              memcpy(data, rxData, rxLen);
+              dataLen = rxLen;
+            case EP_ONDATA_REJECT:
+              // write the ack, ship it, 
+              payload[0] = PK_DEST;
+              payload[1] = EP_SS_ACK;
+              payload[2] = id;
+              uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 3);
+              stackClearSlot(item);
+              stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+              break;
+          }
+      }
+      break;
+    case EP_QUERY:
+      {
+        // beforeQuery, 
+        beforeQuery_cb();
+        // request for our data, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_QUERY_RESP;
+        payload[2] = item->data[ptr + 3];
+        memcpy(&(payload[3]), data, dataLen);
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, dataLen + 3);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_SS_ACK:
+      // acks to us, 
+      for(uint8_t r = 0; r < numRoutes; r ++){
+        if(item->data[ptr + 3] == routes[r]->ackId){
+          switch(routes[r]->state){
+            case EP_TX_AWAITING_ACK:
+              routes[r]->state = EP_TX_IDLE;
+              goto ackEnd;
+            case EP_TX_AWAITING_AND_FRESH:
+              routes[r]->state = EP_TX_FRESH;
+              goto ackEnd;
+            case EP_TX_FRESH:
+            case EP_TX_IDLE:
+            default:
+              // these are nonsense states, likely double-transmits, likely safely ignored,
+              goto ackEnd;
+          } // end switch 
+        }
+      } // end for-each route, if we've reached this point, still dump it;
+      ackEnd:
+      stackClearSlot(item);
+      break;
+    case EP_ROUTE_QUERY_REQ:
+      // MVC request for a route of ours, 
+      {
+        uint8_t id = item->data[ptr + 3];
+        uint16_t r = ts_readUint16(item->data, ptr + 4);
+        uint16_t wptr = 0;
+        // dest, key, id... mode, 
+        payload[wptr ++] = PK_DEST;
+        payload[wptr ++] = EP_ROUTE_QUERY_RES;
+        payload[wptr ++] = id;
+        if(r < numRoutes){
+          payload[wptr ++] = routes[r]->ackMode;
+          // ttl, segsize, 
+          ts_writeUint16(routes[r]->route->ttl, payload, &wptr);
+          ts_writeUint16(routes[r]->route->segSize, payload, &wptr);
+          // path ! 
+          memcpy(&(payload[wptr]), routes[r]->route->path, routes[r]->route->pathLen);
+          wptr += routes[r]->route->pathLen;
+        } else {
+          payload[wptr ++] = 0; // no-route-here, 
+        }
+        // clear request, write reply in place, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, wptr);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_SET_REQ:
+      // MVC request to set a new route, 
+      {
+        // get an ID, 
+        uint8_t id = item->data[ptr + 3];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_SET_RES;
+        payload[2] = id;
+        if(numRoutes + 1 <= ENDPOINT_MAX_ROUTES){
+          // tell call-er it should work, 
+          payload[3] = 1;
+          // gather & set route, 
+          uint8_t mode = item->data[ptr + 4];
+          uint16_t ttl = ts_readUint16(item->data, ptr + 5);
+          uint16_t segSize = ts_readUint16(item->data, ptr + 7);
+          uint8_t* path = &(item->data[ptr + 9]);
+          uint16_t pathLen = item->len - (ptr + 10);
+          OSAP::debug("adding path... w/ ttl " + String(ttl) + " ss " + String(segSize) + " pathLen " + String(pathLen));
+          uint8_t routeIndice = addRoute(new Route(path, pathLen, ttl, segSize), mode);
+          payload[4] = routeIndice;
+        } else {
+          // nope, 
+          payload[3] = 0;
+          payload[4] = 0;
+        }
+        // either case, write the reply, 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 5);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    case EP_ROUTE_RM_REQ:
+      // MVC request to rm a route... 
+      {
+        // msg id, & indice to remove, 
+        uint8_t id = item->data[ptr + 3];
+        uint8_t r = item->data[ptr + 4];
+        // prep a response, 
+        payload[0] = PK_DEST;
+        payload[1] = EP_ROUTE_RM_RES;
+        payload[2] = id;
+        if(r < numRoutes){
+          // RM ok, 
+          payload[3] = 1;
+          // delete / run destructor 
+          delete routes[r];
+          // shift...
+          for(uint8_t i = r; i < numRoutes - 1; i ++){
+            routes[i] = routes[i + 1];
+          }
+          // last is null, 
+          routes[numRoutes] = nullptr;
+          numRoutes --;
+        } else {
+          // rm not-ok
+          payload[3] = 0;
+        }
+        // either case, write reply 
+        uint16_t len = writeReply(item->data, datagram, VT_SLOTSIZE, payload, 4);
+        stackClearSlot(item);
+        stackLoadSlot(this, VT_STACK_DESTINATION, datagram, len);
+      }
+      break;
+    default:
+      OSAP::error("endpoint rx msg w/ unrecognized endpoint key " + String(item->data[ptr + 2]) + " bailing", MINOR);
+      stackClearSlot(item);
+      break;
+  } // end switch... 
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.h b/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.h
new file mode 100644
index 0000000000000000000000000000000000000000..b14e45a64f1346b4e034d853343a336fd75c59aa
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape/vertices/endpoint.h
@@ -0,0 +1,98 @@
+/*
+osap/vertices/endpoint.h
+
+network : software interface
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ENDPOINT_H_
+#define ENDPOINT_H_
+
+#include "../core/vertex.h"
+#include "../core/packets.h"
+
+// ---------------------------------------------- Endpoint Routes, extends OSAP Core Routes 
+
+enum EP_ROUTE_STATES { EP_TX_IDLE, EP_TX_FRESH, EP_TX_AWAITING_ACK, EP_TX_AWAITING_AND_FRESH };
+
+class EndpointRoute {
+  public: 
+    Route* route;
+    uint8_t ackId = 0;
+    uint8_t ackMode = EP_ROUTEMODE_ACKLESS;
+    EP_ROUTE_STATES state = EP_TX_IDLE;
+    uint32_t lastTxTime = 0;
+    uint32_t timeoutLength;
+    // constructor, 
+    EndpointRoute(Route* _route, uint8_t _mode, uint32_t _timeoutLength = 1000);
+    // destructor...
+    ~EndpointRoute(void);
+};
+
+// ---------------------------------------------- Endpoints 
+
+// endpoint handler responses must be one of these enum - 
+enum EP_ONDATA_RESPONSES { EP_ONDATA_REJECT, EP_ONDATA_ACCEPT, EP_ONDATA_WAIT };
+
+// default handlers, 
+EP_ONDATA_RESPONSES onDataDefault(uint8_t* data, uint16_t len);
+boolean beforeQueryDefault(void);
+
+class Endpoint : public Vertex {
+  public:
+    // local data store & length, 
+    uint8_t data[VT_SLOTSIZE];
+    uint16_t dataLen = 0; 
+    // callbacks: on new data & before a query is written out 
+    EP_ONDATA_RESPONSES (*onData_cb)(uint8_t* data, uint16_t len) = onDataDefault;
+    boolean (*beforeQuery_cb)(void) = beforeQueryDefault;
+    // we override vertex loop, 
+    void loop(void) override;
+    void destHandler(stackItem* item, uint16_t ptr) override;
+    // methods,
+    void write(uint8_t* _data, uint16_t len);
+    boolean clearToWrite(void);
+    uint8_t addRoute(Route* _route, uint8_t _mode = EP_ROUTEMODE_ACKLESS, uint32_t _timeoutLength = 1000);
+    // routes, for tx-ing to:
+    EndpointRoute* routes[ENDPOINT_MAX_ROUTES];
+    uint16_t numRoutes = 0;
+    uint16_t lastRouteServiced = 0;
+    uint8_t nextAckID = 77;
+    // base constructor, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len),
+      boolean (*_beforeQuery)(void)
+    );
+    // these are called "delegating constructors" ... best reference is 
+    // here: https://en.cppreference.com/w/cpp/language/constructor 
+    // onData only, 
+    Endpoint(   
+      Vertex* _parent, String _name,
+      EP_ONDATA_RESPONSES (*_onData)(uint8_t* data, uint16_t len)
+    ) : Endpoint ( 
+      _parent, _name, _onData, nullptr
+    ){};
+    // beforeQuery only, 
+    Endpoint(   
+      Vertex* _parent, String _name, 
+      boolean (*_beforeQuery)(void)
+    ) : Endpoint (
+      _parent, _name, nullptr, _beforeQuery
+    ){};
+    // name only, 
+    Endpoint(   
+      Vertex* _parent, String _name
+    ) : Endpoint (
+      _parent, _name, nullptr, nullptr
+    ){};
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/LICENSE.md b/system/firmware/lpf-modular-motion-head/src/osape_arduino/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/README.md b/system/firmware/lpf-modular-motion-head/src/osape_arduino/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da4c90cb6b618b1b8206b0ddf40a240acbaa4ca7
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/README.md
@@ -0,0 +1,7 @@
+## OSAP Arduino
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+It does not do anything on its own; this one builds helper classes to turn Arduino `Serial` and `Wire` objects into *virtual ports* and *virtual busses* respectively. 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.cpp b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8634694ff54bf2f45fdc704f3fd961168f4620bb
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.cpp
@@ -0,0 +1,77 @@
+/*
+arduino-ports/vp_arduinoWire.cpp
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#include "vb_arduinoWire.h"
+
+// static stash: same per instance, 
+uint8_t stash[32];
+uint8_t stashLen = 0;
+
+VBus_ArduinoWire::VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr
+) : VBus ( _parent, _name ) {
+  wire = _wire;
+  ownRxAddr = _ownRxAddr;
+}
+
+void VBus_ArduinoWire::begin(void){
+  wire->begin(ownRxAddr);
+  wire->onReceive(this->onRecieve);
+}
+
+void VBus_ArduinoWire::onRecieve(int count){
+  Wire.readBytes(stash, count);
+  stashLen = count;
+}
+
+void VBus_ArduinoWire::loop(void){
+  // check incoming, 
+  if(stashLen > 0){
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      stackLoadSlot(this, VT_STACK_ORIGIN, stash, stashLen);
+    }
+    stashLen = 0;
+  }
+}
+
+void VBus_ArduinoWire::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  digitalWrite(A1, HIGH);
+  // this'll be the big hangup, 
+  if(len > 32) return;
+  // this might guard, if we are already rx'ing... 
+  if(wire->available()) return;
+  // become host, 
+  wire->end();
+  wire->begin();
+  // transmit, 
+  wire->beginTransmission(rxAddr);
+  wire->write(data, len);
+  uint8_t res = wire->endTransmission();
+  // become guest again, 
+  wire->end();
+  wire->begin(ownRxAddr);
+  // check, 
+  //if(res != 0) 
+  // DEBUG("res " + String(res) + " txd " + String(len));
+  digitalWrite(A1, LOW);
+}
+
+boolean VBus_ArduinoWire::cts(uint8_t rxAddr){
+  return true;
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.h b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.h
new file mode 100644
index 0000000000000000000000000000000000000000..b098634545544070b65a46e1106719567f2fbe5b
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vb_arduinoWire.h
@@ -0,0 +1,43 @@
+/*
+arduino-ports/vp_arduinoWire.h
+
+turns Wire instances into competent bus link layers for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "../osap_config.h"
+
+#ifdef INCLUDE_WIRE_VPORT
+
+#ifndef ARDU_WIRELINK_H_
+#define ARDU_WIRELINK_H_
+
+#include <Arduino.h>
+#include <Wire.h>
+#include "../osape/core/vertex.h"
+
+#define WIRELINK_BUFSIZE 255 
+
+class VBus_ArduinoWire : public VBus {
+  public:
+    void begin(void);
+    // -------------------------------- our own loop, cts, and send... 
+    void loop(void) override; 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override; 
+    boolean cts(uint8_t rxAddr) override; 
+    // -------------------------------- data 
+    TwoWire* wire;
+    static void onRecieve(int count);
+    // -------------------------------- constructors
+    VBus_ArduinoWire(Vertex* _parent, String _name, TwoWire* _wire, uint8_t _ownRxAddr);
+};
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.cpp b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f71fe57592eccba322baf9108b38d068a2aed544
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.cpp
@@ -0,0 +1,174 @@
+/*
+arduino-ports/ardu-vport.h
+
+turns serial objects into competent link layers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "vp_arduinoSerial.h"
+#include "./osape/utils/cobs.h"
+#include "../osape/core/osap.h"
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Uart* _uart
+) : VPort ( _parent, _name ){
+  stream = _uart; // should convert Uart* to Stream*, as Uart inherits stream 
+  uart = _uart; 
+}
+
+VPort_ArduinoSerial::VPort_ArduinoSerial( Vertex* _parent, String _name, Serial_* _usbcdc
+) : VPort ( _parent, _name ){
+  stream = _usbcdc;
+  usbcdc = _usbcdc;
+}
+
+void VPort_ArduinoSerial::begin(uint32_t baudRate){
+  if(uart != nullptr){
+    uart->begin(baudRate);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(baudRate); 
+  }
+}
+
+void VPort_ArduinoSerial::begin(void){
+  if(uart != nullptr){
+    uart->begin(1000000);
+  } else if (usbcdc != nullptr){
+    usbcdc->begin(9600);  // baud ignored on cdc begin  
+  }
+}
+
+// link packets are max 256 bytes in length, including the 0 delimiter 
+// structured like:
+// checksum | pck/ack key | pck id | cobs encoded data | 0 
+
+void VPort_ArduinoSerial::loop(void){
+  // byte injestion: think of this like the rx interrupt stage, 
+  while(stream->available()){
+    // read byte into the current stub, 
+    rxBuffer[rxBufferWp ++] = stream->read();
+    if(rxBuffer[rxBufferWp - 1] == 0){
+      // always reset keepalive last-rx time, 
+      lastRxTime = millis();
+      // 1st, we checksum:
+      if(rxBuffer[0] != rxBufferWp){ 
+        OSAP::error("serLink bad checksum, cs: " + String(rxBuffer[0]) + " wp: " + String(rxBufferWp), MINOR);
+      } else {
+        // acks, packs, or broken things 
+        switch(rxBuffer[1]){
+          case SERLINK_KEY_PCK:
+            // dirty guard for retransmitted packets, 
+            if(rxBuffer[2] != lastIdRxd){
+              inAwaitingId = rxBuffer[2]; // stash ID 
+              inAwaitingLen = cobsDecode(&(rxBuffer[3]), rxBufferWp - 2, inAwaiting); // fill inAwaiting 
+            } else {
+              OSAP::error("serLink double rx", MINOR);
+            }
+            break;
+          case SERLINK_KEY_ACK:
+            if(rxBuffer[2] == outAwaitingId){
+              outAwaitingLen = 0;
+            }
+            break;
+          case SERLINK_KEY_KEEPALIVE:
+            // noop, 
+            break;
+          default:
+            // makes no sense, 
+            break;
+        }
+      }
+      // always reset on delimiter, 
+      rxBufferWp = 0;
+    }
+  } // end while-receive 
+
+  // check insertion & genny the ack if we can 
+  if(inAwaitingLen && stackEmptySlot(this, VT_STACK_ORIGIN) && !ackIsAwaiting){
+    stackLoadSlot(this, VT_STACK_ORIGIN, inAwaiting, inAwaitingLen);
+    ackIsAwaiting = true;
+    ackAwaiting[0] = 4;                 // checksum still, innit 
+    ackAwaiting[1] = SERLINK_KEY_ACK;   // it's an ack bruv 
+    ackAwaiting[2] = inAwaitingId;      // which pck r we akkin m8 
+    ackAwaiting[3] = 0;                 // delimiter 
+    inAwaitingLen = 0;
+  }
+
+  // check & execute actual tx 
+  checkOutputStates();
+}
+
+void VPort_ArduinoSerial::send(uint8_t* data, uint16_t len){
+  //digitalWrite(A4, !digitalRead(A4));
+  // double guard?
+  if(!cts()) return;
+  // setup, 
+  outAwaiting[0] = len + 5;               // pck[0] is checksum = len + checksum + cobs start + cobs delimit + ack/pack + id 
+  outAwaiting[1] = SERLINK_KEY_PCK;       // this ones a packet m8 
+  outAwaitingId ++; if(outAwaitingId == 0) outAwaitingId = 1;
+  outAwaiting[2] = outAwaitingId;         // an id     
+  cobsEncode(data, len, &(outAwaiting[3]));  // encode 
+  outAwaiting[len + 4] = 0;               // stuff delimiter, 
+  outAwaitingLen = outAwaiting[0];        // track... 
+  // transmit attempts etc 
+  outAwaitingNTA = 0;
+  outAwaitingLTAT = 0;
+  // try it 
+  checkOutputStates();                    // try / start write 
+}
+
+// we are CTS if outPck is not occupied, 
+boolean VPort_ArduinoSerial::cts(void){
+  return (outAwaitingLen == 0);
+}
+
+// we are open if we've heard back lately, 
+boolean VPort_ArduinoSerial::isOpen(void){
+  return (millis() - lastRxTime < SERLINK_KEEPALIVE_RX_TIME && lastRxTime != 0);
+}
+
+void VPort_ArduinoSerial::checkOutputStates(void){
+  if(ackIsAwaiting && txBufferLen == 0){   // can we ack? 
+    memcpy(txBuffer, ackAwaiting, 4);
+    txBufferLen = 4;
+    lastTxTime = millis();
+    txBufferRp = 0;
+    ackIsAwaiting = false;
+  } else if(outAwaitingLen > 0 && txBufferLen == 0){   // would we be clear to tx ? 
+    // check retransmit cases, 
+    if(outAwaitingLTAT == 0 || outAwaitingLTAT + SERLINK_RETRY_TIME < micros()){
+      memcpy(txBuffer, outAwaiting, outAwaitingLen);
+      outAwaitingLTAT = micros();
+      txBufferLen = outAwaitingLen;
+      lastTxTime = millis();
+      txBufferRp = 0;
+      outAwaitingNTA ++;
+    } 
+    // check if last attempt, 
+    if(outAwaitingNTA >= SERLINK_RETRY_MACOUNT){
+      outAwaitingLen = 0;
+    }
+  } else if (millis() - lastTxTime > SERLINK_KEEPALIVE_TX_TIME && txBufferLen == 0){
+    //OSAP::debug("keepalive-ing " + name + " " + String(isOpen()));
+    memcpy(txBuffer, keepAlivePacket, 3);
+    txBufferLen = 3;
+    lastTxTime = millis();
+  }
+  // finally, we write out so long as we can: 
+  // we aren't guaranteed to get whole pckts out in each fn call 
+  while(stream->availableForWrite() && txBufferLen != 0){
+    // output next byte, 
+    stream->write(txBuffer[txBufferRp ++]);
+    // check for end of buffer; reset transmit states if so 
+    if(txBufferRp >= txBufferLen) {
+      txBufferLen = 0; 
+      txBufferRp = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.h b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa518aabc7e8905a85abf8ec07d4a2138b2f10f2
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_arduino/vp_arduinoSerial.h
@@ -0,0 +1,88 @@
+/*
+arduino-ports/vp_arduinoSerial.h
+
+turns arduino serial objects into competent link layers, for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef ARDU_SERLINK_H_
+#define ARDU_SERLINK_H_
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+#define SERLINK_BUFSIZE 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+#define SERLINK_SEGSIZE SERLINK_BUFSIZE - 5
+// packet keys; 
+#define SERLINK_KEY_PCK 170  // 0b10101010
+#define SERLINK_KEY_ACK 171  // 0b10101011
+#define SERLINK_KEY_KEEPALIVE 173 
+// retry settings 
+#define SERLINK_RETRY_MACOUNT 2
+#define SERLINK_RETRY_TIME 100000  // microseconds 
+#define SERLINK_KEEPALIVE_TX_TIME 800 // milliseconds 
+#define SERLINK_KEEPALIVE_RX_TIME 1200 // ms 
+
+#define SERLINK_LIGHT_ON_TIME 100 // in ms 
+
+// note that we use uint8_t write ptrs / etc: and a size of 255, 
+// so we are never dealing w/ wraps etc, god bless 
+
+class VPort_ArduinoSerial : public VPort {
+  public:
+    // arduino std begin 
+    void begin(uint32_t baud);
+    void begin(void);
+    // -------------------------------- our own gd send & cts & loop fns, 
+    void loop(void) override;
+    void checkOutputStates(void);
+    void send(uint8_t* data, uint16_t len) override;
+    boolean cts(void) override;
+    boolean isOpen(void) override;
+    // -------------------------------- Data 
+    // Uart & USB are both Stream classes, 
+    Stream* stream;
+    // we have an overloaded constructor w/ uart or Serial_, the usb class 
+    Uart* uart = nullptr;
+    Serial_* usbcdc = nullptr; 
+    // incoming, always kept clear to receive: 
+    uint8_t rxBuffer[SERLINK_BUFSIZE];
+    uint8_t rxBufferWp = 0;
+    // keepalive state, 
+    uint32_t lastRxTime = 0;
+    uint32_t lastTxTime = 0;
+    uint8_t keepAlivePacket[3] = {3, SERLINK_KEY_KEEPALIVE, 0};
+    // guard on double transmits 
+    uint8_t lastIdRxd = 0;
+    // incoming stash
+    uint8_t inAwaiting[SERLINK_BUFSIZE];
+    uint8_t inAwaitingId = 0;
+    uint8_t inAwaitingLen = 0;
+    // outgoing ack, 
+    uint8_t ackAwaiting[4];
+    boolean ackIsAwaiting = false;
+    // outgoing await,
+    uint8_t outAwaiting[SERLINK_BUFSIZE];
+    uint8_t outAwaitingId = 1;
+    uint8_t outAwaitingLen = 0;
+    uint8_t outAwaitingNTA = 0;
+    unsigned long outAwaitingLTAT = 0;
+    // outgoing buffer,
+    uint8_t txBuffer[SERLINK_BUFSIZE];
+    uint8_t txBufferLen = 0;
+    uint8_t txBufferRp = 0;
+    // -------------------------------- Constructors 
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Uart* _uart);
+    VPort_ArduinoSerial(Vertex* _parent, String _name, Serial_* _usbcdc);
+};
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/README.md b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e5a9fae5795a46730372cd9533efa958bc12c2e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/README.md
@@ -0,0 +1,6 @@
+## UART-Clocked Bus Submodule 
+
+https://gitlab.cba.mit.edu/jakeread/ucbus 
+https://github.com/jakeread/ucbus 
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.cpp b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2bd443fa0154b4e62e4392611b7afabb257fb
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.cpp
@@ -0,0 +1,510 @@
+/*
+osap/drivers/ucBusDrop.cpp
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include "ucBusDipConfig.h"
+#include "../indicators.h"
+#include "../osape/core/osap.h"
+
+// recieve buffers
+uint8_t recieveBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t recieveBufferWp[UB_CH_COUNT];
+// tracking did-last-msg have token,
+volatile boolean lastWordHadToken[UB_CH_COUNT];
+
+// stash buffers (have to ferry data from rx buffer -> here immediately on rx, else next word can overwrite)
+uint8_t inBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t inBufferLen[UB_CH_COUNT];
+
+// output buffer 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// receive word
+UCBUS_HEADER_Type inHeader = { .bytes = { 0,0 } };
+volatile uint8_t inWordWp = 0;
+uint8_t inWord[UB_HEAD_BYTES_PER_WORD];
+
+// outgoing word 
+UCBUS_HEADER_Type outHeader = { .bytes = { 0,0 } };
+uint8_t outWord[UB_DROP_BYTES_PER_WORD];
+volatile uint8_t outWordRp = 0;
+
+// reciprocal buffer space, for flowcontrol 
+volatile uint8_t rcrxb[UB_CH_COUNT];
+// last-time-rx'd 
+volatile uint32_t lastRxTime = 0;
+
+// our physical bus address, 
+volatile uint8_t id = 0;
+
+// available time count, in bus tick units 
+volatile uint16_t timeTick = 0;
+volatile uint64_t timeBlink = 0;
+uint16_t blinkTime = 1000;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// we need to track interrupt states as well as setting the flags in the micro, 
+// since the D21 fires only one ISR for all of the flags;
+volatile boolean txcISR = false;
+volatile boolean dreISR = false;
+
+#define DRE_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE; dreISR = true
+#define DRE_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; dreISR = false 
+#define TXC_ISR_ON UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; txcISR = true 
+#define TXC_ISR_OFF UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC; txcISR = false 
+
+#ifdef UCBUS_IS_D51 
+// ------------------------------------ D51 SPECIFIC 
+// hardware init (file scoped)
+void setupBusDropUART(void){
+  // set driver output LO to start: tri-state 
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DRIVER_DISABLE;
+  // set receiver output on, forever: LO to set on 
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor should be set only on one drop, 
+  // or none and physically with a 'tail' cable, or something? 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  if(dip_readPin1()){
+    UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  } else {
+    UB_TE_PORT.OUTSET.reg = UB_TE_BM;
+  }
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  	// unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ctrla 
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD;
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0);
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // enable even parity 
+  // ctrlb 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable rx interrupt, disable dre, txc 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // to enable tx complete, 
+  //UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // now watch transmit complete
+}
+
+// DRE handler 
+void SERCOM1_0_Handler(void){
+  ucBusDrop_dreISR();
+}
+
+// TXC handler 
+void SERCOM1_1_Handler(void){
+  ucBusDrop_txcISR();
+}
+
+void SERCOM1_2_Handler(void){
+	ucBusDrop_rxISR();
+}
+// ------------------------------------ END D51 SPECIFIC 
+#endif 
+
+#ifdef UCBUS_IS_D21 
+// ------------------------------------ D21 SPECIFIC 
+void setupBusDropUART(void){
+  // ------------------------------------------ USART PIN CONFIG
+  // setup pins as output or inputs,
+  UB_PORT.DIRSET.reg = UB_TXBM;
+  UB_PORT.DIRCLR.reg = UB_RXBM;
+  // pincfg using wrconfig write, s/o
+  // https://community.atmel.com/forum/sam-d21-spi-interface-bare-code
+  PORT_WRCONFIG_Type wrconfig;  // make new write config object,
+  wrconfig.bit.WRPMUX = 1;      // it will write to pmux
+  wrconfig.bit.WRPINCFG = 1;    // it will write to pinconfig
+  wrconfig.bit.PMUX = MUX_PA16C_SERCOM1_PAD0;  // with this pmux setting
+                                                // (putting 16 on c, for ser1)
+  wrconfig.bit.PMUXEN = 1;                     // enabling pin muxing
+  wrconfig.bit.HWSEL = 1;  // writing to the upper half of the pins
+                            // and (below) writing these pins, masked and
+                            // shifted into the lower half
+  wrconfig.bit.PINMASK = (uint16_t)((UB_TXBM | UB_RXBM) >> 16);
+  UB_PORT.WRCONFIG.reg = wrconfig.reg;  // here's the one-shot write, using prep above
+  // ------------------------------------------ Transmit Driver / Recieve
+  // Driver Enable
+  UB_DE_SETUP;
+  UB_RE_SETUP;
+  // ------------------------------------------ SPI CONFIG
+  // now, lettuce unmask the peripheral SER1
+  PM->APBCMASK.reg |= PM_APBCMASK_SERCOM1;
+  // hook the peripheral up to our main CPU clock, which is running at 48mHz
+  // on the D21
+  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 |
+                      GCLK_CLKCTRL_ID_SERCOM1_CORE;
+  while (GCLK->STATUS.bit.SYNCBUSY);
+  // now we can setup the actual sercom, first do a reset for posterity and
+  // await complete
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.SWRST);
+  // pinout: TX on SERx-0, RX on SERx-2
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_DORD |     // lsb first
+                            SERCOM_USART_CTRLA_MODE(1) |  // internal clock
+                            SERCOM_USART_CTRLA_TXPO(0) |  // tx on SERx-0
+                            SERCOM_USART_CTRLA_RXPO(UB_RXPO);  // rx on SERx-3
+  // enable reciever, transmit,
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN;
+  // set BAUD:
+  UB_SER_USART.BAUD.reg = SERCOM_USART_BAUD_BAUD(ub_baud_val);
+  // we will use interrupts: not the highest priority (0), just under. 
+  NVIC_EnableIRQ(SERCOM1_IRQn);
+  NVIC_SetPriority(SERCOM1_IRQn, 1);
+  // rx interrupt always
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+  // UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  // ok I think that's it?
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  while (UB_SER_USART.SYNCBUSY.bit.ENABLE);
+}
+
+void SERCOM1_Handler(void) {
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_RXC) {
+    ucBusDrop_rxISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_DRE && dreISR) {
+    ucBusDrop_dreISR();
+  } 
+  if (UB_SER_USART.INTFLAG.reg & SERCOM_USART_INTFLAG_TXC && txcISR){
+    ucBusDrop_txcISR();
+  } 
+} // ------------------------------------------------------ END SERCOM ISR
+// ------------------------------------ END D21 SPECIFIC 
+#endif 
+
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID) {
+  #ifdef UCBUS_IS_D51
+  dip_setup();
+  if(useDipPick){
+    // set our id, 
+    id = dip_readLowerFive(); // should read lower 4, now that cha / chb 
+  } else {
+    id = ID;
+  }
+  #endif 
+  #ifdef UCBUS_IS_D21
+  id = ID;
+  #endif 
+  if(id > 31){ id = 31; }   // max 31 drops, logical addresses 1 - 31
+  if(id == 0){ id = 1; }    // 0 'tap' is the clk reset, bump up... maybe cause confusion: instead could flash err light 
+  // setup input / etc buffers 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    recieveBufferWp[ch] = 0;
+    inBufferLen[ch] = 0;
+    outBufferRp[ch] = 0;
+    outBufferLen[ch] = 0;
+    rcrxb[ch] = 0;
+  }
+  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the hardware 
+  setupBusDropUART();
+}
+
+uint16_t ucBusDrop_getOwnID(void){
+  return id;
+}
+
+void ucBusDrop_rxISR(void){
+  // ------------------------------------------------------ DATA INGEST
+  // get the data 
+  uint8_t data = UB_SER_USART.DATA.reg;
+  inWord[inWordWp ++] = data;
+  // tracking delineation 
+  if(inWordWp >= UB_HEAD_BYTES_PER_WORD){
+    // track keepalive 
+    lastRxTime = millis();
+    // always reset, never overwrite inWord[] tail
+    inWordWp = 0;
+    // is lastchar the rarechar ?
+    if(inWord[UB_HEAD_BYTES_PER_WORD - 1] == UCBUS_RARECHAR){
+      // carry on, 
+    } else {
+      // restart on appearance of rarechar 
+      for(uint8_t b = 0; b < UB_HEAD_BYTES_PER_WORD; b ++){
+        if(inWord[b] == UCBUS_RARECHAR){
+          inWordWp = UB_HEAD_BYTES_PER_WORD - 1 - b;
+          // in case the above ^ causes some wrapping case (?) don't think it does though 
+          if(inWordWp >= UB_HEAD_BYTES_PER_WORD) inWordWp = 0;
+          return;
+        }
+      }
+    }
+  } else {
+    // was just data byte, bail for now 
+    return;
+  }
+  // ------------------------------------------------------ TERMINAL BYTE CASE 
+  // blink on count-of-words:
+  timeTick ++;
+  timeBlink ++;
+  if(timeBlink >= blinkTime){
+    CLKLIGHT_TOGGLE; 
+    timeBlink = 0;
+  }
+  // extract the header, 
+  inHeader.bytes[0] = inWord[0];
+  inHeader.bytes[1] = inWord[1];
+  // now, check for our-rx:
+  if(inHeader.bits.DROPTAP == id){  // -------------------- OUR TAP, TX CASE 
+    // read-in fc states, 
+    rcrxb[0] = inHeader.bits.CH0FC;
+    rcrxb[1] = inHeader.bits.CH1FC;
+    // reset out header,
+    outHeader.bytes[0] = 0; 
+    outHeader.bytes[1] = 0;
+    // write outgoing flowcontrol terms: if we have unread buffers on these chs, zero space avail:
+    outHeader.bits.CH0FC = (inBufferLen[0] ?  0 : 1);
+    outHeader.bits.CH1FC = (inBufferLen[1] ?  0 : 1);
+    // write also our drop tap...
+    outHeader.bits.DROPTAP = id;
+    // check about tx state, 
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      if(outBufferLen[ch] && rcrxb[ch] > 0){
+        // can tx this ch, 
+        uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+        if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+        // can fill ch-output, 
+        outHeader.bits.CHSELECT = ch;
+        outHeader.bits.TOKENS = numTx;
+        // fill bytes,
+        uint8_t* outB = outBuffer[ch];
+        uint16_t outBRp = outBufferRp[ch];
+        for(uint8_t b = 0; b < numTx; b ++){
+          outWord[b + 2] = outB[outBRp + b];  // fill from ob[2], ob[0] and ob[1] are header 
+        }
+        outBufferRp[ch] += numTx;
+        // if numTx < data bytes / frame, packet terminates this word, we reset 
+        if(numTx < UB_DATA_BYTES_PER_WORD){
+          outBufferLen[ch] = 0;
+          outBufferRp[ch] = 0;
+        }
+        break; // don't check next ch, 
+      }
+    }
+    // stuff header -> word
+    outWord[0] = outHeader.bytes[0];
+    outWord[1] = outHeader.bytes[1];
+    // now setup the transmit action:
+    // set driver on, ship 1st byte, tx rest on DRE edges 
+    outWordRp = 1; // next is [1]
+    UB_DRIVER_ENABLE;
+    UB_SER_USART.DATA.reg = outWord[0];
+    DRE_ISR_ON;
+  } // ---------------------------------------------------- END TX CASE 
+
+  // ------------------------------------------------------ BEGIN RX TERMS 
+  // the ch that head tx'd to 
+  uint8_t rxCh = inHeader.bits.CHSELECT;
+  // and # bytes tx'd here 
+  uint8_t numToken = inHeader.bits.TOKENS;
+  // check for broken numToken count,
+  if(numToken > UB_DATA_BYTES_PER_WORD) { 
+    OSAP::error("ucbus-drop outsize numToken rx", MINOR); 
+    return; 
+  }
+  // don't overfill recieve buffer: 
+  if(recieveBufferWp[rxCh] + numToken > UB_BUFSIZE){
+    recieveBufferWp[rxCh] = 0;
+    OSAP::error("ucbus-drop rx overfull buffer", MINOR);
+    return;
+  }
+  // so let's see, if we have any we write them in:
+  if(numToken > 0){
+    uint8_t* rxB = recieveBuffer[rxCh];
+    uint16_t rxBWp = recieveBufferWp[rxCh]; 
+    for(uint8_t i = 0; i < numToken; i ++){
+      rxB[rxBWp + i] = inWord[2 + i];
+    }
+    recieveBufferWp[rxCh] += numToken;
+    // set in-packet state,
+    lastWordHadToken[rxCh] = true;
+  }
+  // to find the edge, if we have numToken < numDataBytes and have at least one previous
+  // token in stream, we have pckt edge 
+  if((numToken < UB_DATA_BYTES_PER_WORD) && lastWordHadToken[rxCh]){
+    // reset token edge
+    lastWordHadToken[rxCh] = false;
+    // pckt edge on this ch, shift recieveBuffer -> inBuffer and reset write pointer 
+    // unfortunately we have to do this literal-swap thing (some memcpy coming up here), 
+    // but should be able to use a pointer-swapping approach later. here we check if the pck 
+    // is actually for us, then if we can accept it (fc not violated) and then swap it in:
+    if(recieveBuffer[rxCh][0] == id || rxCh == 0){
+      // we should accept this, can we?
+      if(inBufferLen[rxCh] != 0){ // failed to clear before new arrival, FC has failed 
+        recieveBufferWp[rxCh] = 0;
+        OSAP::error("ucbus-drop rx FC fails on ch " + String(rxCh), MINOR);
+        return;
+      } // end check-for-overwrite 
+      // copy from rxbuffer to inbuffer, it's ours... now FC will go lo, head should not tx *to us*
+      // before it is cleared with ucBusDrop_readB()
+      memcpy(inBuffer[rxCh], recieveBuffer[rxCh], recieveBufferWp[rxCh]);
+      inBufferLen[rxCh] = recieveBufferWp[rxCh];
+      recieveBufferWp[rxCh] = 0;
+      // if CH0, fire "RT" on-rx interrupt, this is where we should want RTOS in the future 
+      if(rxCh == 0){
+        // ucBusDrop_onPacketARx(&(inBuffer[0][1]), inBufferLen[0] - 1);
+        // assuming the interrupt is the exit for time being,
+        // inBufferLen[0] = 0;
+      }
+      //DEBUG1PIN_OFF;
+    } else {
+      // packet wasn't for us, ignore 
+      recieveBufferWp[rxCh] = 0;
+    }
+  } // ---------------------------------------------------- END RX TERMS
+
+  // finally (and a bit yikes) we call the onRxISR on *every* word, that's our 
+  // synced system clock: fair warning though, we're firing this pretty late
+  // esp. if we have also this time transmitted, read in a packet, etc... yikes 
+  ucBusDrop_onRxISR();
+} // end rx-isr 
+
+void ucBusDrop_dreISR(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_DROP_BYTES_PER_WORD){
+    DRE_ISR_OFF; // clear tx-empty int.
+    TXC_ISR_ON;  // set tx-complete int.
+  } 
+}
+
+void ucBusDrop_txcISR(void){
+  UB_SER_USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;   // clear flag (so interrupt not called again)
+  TXC_ISR_OFF;
+  UB_DRIVER_DISABLE;
+}
+
+// -------------------------------------------------------- ASYNC API
+
+boolean ucBusDrop_ctrB(void){
+  // clear to read a packet when this buffer occupied... 
+  return (inBufferLen[1] > 0);
+}
+
+boolean ucBusDrop_ctrA(void){
+  // likewise
+  return (inBufferLen[0] > 0);
+}
+
+size_t ucBusDrop_readB(uint8_t *dest){
+  if(!ucBusDrop_ctrB()) return 0;
+  // to read-out, we rm the 0th byte which is addr information
+  size_t len = inBufferLen[1] - 1;
+  memcpy(dest, &(inBuffer[1][1]), len);
+  inBufferLen[1] = 0; // now it's empty 
+  return len;
+}
+
+size_t ucBusDrop_readA(uint8_t* dest){
+  if(!ucBusDrop_ctrA()) return 0;
+  // we read out the whole gd thing,
+  size_t len = inBufferLen[0];
+  memcpy(dest, &(inBuffer[0]), len);
+  inBufferLen[0] = 0; // now empty 
+  return len;
+}
+
+boolean ucBusDrop_ctsB(void){
+  if(outBufferLen[1] == 0 && rcrxb[1] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusDrop_isPresent(uint8_t drop){
+  // can't tx anywhere other than to head, 
+  if(drop > 0) return false;
+  return (millis() - lastRxTime < UB_KEEPALIVE_TIME);
+}
+
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len){
+  if(!ucBusDrop_ctsB()) return;
+  // we don't need to decriment our count of the remote rcrxb here
+  // because we get an update from the head on their actual rcrxb *each time we are tapped*
+  // however, we cannot tx more than the bufsize, bruh 
+  if(len > UB_BUFSIZE) return;
+  // copy it into the outBuffer, 
+  memcpy(&(outBuffer[1]), data, len);
+  // needs to be interrupt safe: transmit could start between these lines
+  __disable_irq();
+  outBufferLen[1] = len;
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..281f430bd6ced5264fd9607ce8f51a1a9c31cbab
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusDrop.h
@@ -0,0 +1,51 @@
+/*
+osap/drivers/ucBusDrop.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_DROP_H_
+#define UCBUS_DROP_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup 
+void ucBusDrop_setup(boolean useDipPick, uint8_t ID);
+uint16_t ucBusDrop_getOwnID(void);
+
+// isrs 
+void ucBusDrop_rxISR(void);
+void ucBusDrop_dreISR(void);
+void ucBusDrop_txcISR(void);
+
+// handlers (define in main.cpp, these are application interfaces)
+void ucBusDrop_onRxISR(void);
+void ucBusDrop_onPacketARx(uint8_t* inBufferA, volatile uint16_t len);
+
+// the api, eh 
+boolean ucBusDrop_ctrB(void);
+size_t ucBusDrop_readB(uint8_t* dest);
+boolean ucBusDrop_ctrA(void);
+size_t ucBusDrop_readA(uint8_t* dest);
+
+// drop cannot tx to channel A
+boolean ucBusDrop_ctsB(void); // true if tx buffer empty, 
+boolean ucBusDrop_isPresent(uint8_t rxAddr);
+void ucBusDrop_transmitB(uint8_t *data, uint16_t len);
+
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.cpp b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..854e488395920dd19b812643282ddbf9c7f3ae25
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.cpp
@@ -0,0 +1,386 @@
+/*
+osap/drivers/ucBusHead.cpp
+
+uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include "../osape/core/osap.h"
+#include "./utils_samd51/peripheral_nums.h"
+
+// input buffers / space 
+uint8_t inBuffer[UB_CH_COUNT][UB_MAX_DROPS][UB_BUFSIZE];   // per-drop incoming bytes: 0 will be empty always, no drop here
+volatile uint16_t inBufferWp[UB_CH_COUNT][UB_MAX_DROPS];   // per-drop incoming write pointer
+volatile uint16_t inBufferLen[UB_CH_COUNT][UB_MAX_DROPS];  // per-drop incoming bytes, len of, set when EOP detected
+volatile boolean lastWordHadToken[UB_CH_COUNT][UB_MAX_DROPS];
+
+// transmit buffers 
+uint8_t outBuffer[UB_CH_COUNT][UB_BUFSIZE];
+volatile uint16_t outBufferRp[UB_CH_COUNT];
+volatile uint16_t outBufferLen[UB_CH_COUNT];
+
+// flow control, per ch per drop 
+volatile uint8_t rcrxb[UB_CH_COUNT][UB_MAX_DROPS];     // if 0 donot tx on this ch / this drop 
+
+// last-rx'd-time, per drop presence-detect, 
+volatile uint32_t lastRxTime[UB_MAX_DROPS];
+
+// currently 'tapped' drop - we loop thru bus drops, 
+volatile uint8_t currentDropTap = 1; // drop we are currently 'txing' to / drop that will reply on this cycle
+volatile uint8_t lastDropTap = 1; 
+
+// outgoing word / stuff info 
+volatile UCBUS_HEADER_Type outHeader = { .bytes = { 0, 0 } };
+uint8_t outWord[UB_HEAD_BYTES_PER_WORD];                // this goes on-the-line, 
+volatile uint8_t outWordRp = 0;
+
+// incoming word 
+volatile UCBUS_HEADER_Type inHeader = { .bytes = { 0, 0 } };
+uint8_t inWord[UB_DROP_BYTES_PER_WORD];
+uint8_t inWordWp = 0;
+
+// baudrate 
+uint32_t ub_baud_val = 0;
+
+// uart init (file scoped)
+void setupBusHeadUART(void){
+  // driver output is always on at head, set HI to enable
+  UB_DE_PORT.DIRSET.reg = UB_DE_BM;
+  UB_DE_PORT.OUTSET.reg = UB_DE_BM;
+  // receive output is always on at head, set LO to enable
+  UB_RE_PORT.DIRSET.reg = UB_RE_BM;
+  UB_RE_PORT.OUTCLR.reg = UB_RE_BM;
+  // termination resistor for receipt on bus head is always on, set LO to enable 
+  UB_TE_PORT.DIRSET.reg = UB_TE_BM;
+  UB_TE_PORT.OUTCLR.reg = UB_TE_BM;
+  // rx pin setup
+  UB_COMPORT.DIRCLR.reg = UB_RXBM;
+  UB_COMPORT.PINCFG[UB_RXPIN].bit.PMUXEN = 1;
+  if(UB_RXPIN % 2){
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_RXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_RXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_RXPERIPHERAL);
+  }
+  // tx
+  UB_COMPORT.DIRCLR.reg = UB_TXBM;
+  UB_COMPORT.PINCFG[UB_TXPIN].bit.PMUXEN = 1;
+  if(UB_TXPIN % 2){
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXO(UB_TXPERIPHERAL);
+  } else {
+    UB_COMPORT.PMUX[UB_TXPIN >> 1].reg |= PORT_PMUX_PMUXE(UB_TXPERIPHERAL);
+  }
+  // ok, clocks, first line au manuel
+  // unmask clocks 
+	MCLK->APBAMASK.bit.SERCOM1_ = 1;
+  GCLK->GENCTRL[UB_GCLKNUM_PICK].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
+  while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(UB_GCLKNUM_PICK));
+	GCLK->PCHCTRL[UB_SERCOM_CLK].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(UB_GCLKNUM_PICK);
+  // then, sercom: disable and then perform software reset
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 0;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  UB_SER_USART.CTRLA.bit.SWRST = 1;
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST);
+  while(UB_SER_USART.SYNCBUSY.bit.SWRST || UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  // ok, CTRLA:
+  UB_SER_USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD; // data order (1: lsb first) and mode (?) 
+  UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_RXPO(UB_RXPO) | SERCOM_USART_CTRLA_TXPO(0); // rx and tx pinout options 
+  //UB_SER_USART.CTRLA.reg |= SERCOM_USART_CTRLA_FORM(1); // turn on parity: parity is even by default (set in CTRLB), leave that 
+  // CTRLB has sync bit, 
+  while(UB_SER_USART.SYNCBUSY.bit.CTRLB);
+  // recieve enable, txenable, character size 8bit, 
+  UB_SER_USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
+  // CTRLC: setup 32 bit on read and write:
+  // UBH_SER_USART.CTRLC.reg = SERCOM_USART_CTRLC_DATA32B(3); 
+	// enable interrupts 
+	NVIC_EnableIRQ(SERCOM1_2_IRQn); // rx interrupts 
+  NVIC_EnableIRQ(SERCOM1_1_IRQn); // transmit complete interrupt 
+	NVIC_EnableIRQ(SERCOM1_0_IRQn); // data register empty interrupts 
+	// set baud 
+  UB_SER_USART.BAUD.reg = ub_baud_val;
+  // and finally, a kickoff
+  while(UB_SER_USART.SYNCBUSY.bit.ENABLE);
+  UB_SER_USART.CTRLA.bit.ENABLE = 1;
+  // enable the RXC interrupt, disable TXC, DRE
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE | SERCOM_USART_INTENCLR_TXC;
+}
+
+// TX Handler, for second bytes initiated by timer, 
+// void SERCOM1_0_Handler(void){
+// 	ucBusHead_txISR();
+// }
+
+// startup, 
+void ucBusHead_setup(void){
+  // clear buffers to begin, also set lastRxTime to zero for each, 
+  for(uint8_t d = 0; d < UB_MAX_DROPS; d ++){
+    lastRxTime[d] = 0;
+    for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+      outBufferLen[ch] = 0;
+      outBufferRp[ch] = 0;
+      inBufferLen[ch][d] = 0; // zero all input buffers, write-in pointers
+      inBufferWp[ch][d] = 0;
+      rcrxb[ch][d] = 0;       // assume zero space to tx to all drops until they report otherwise 
+      lastWordHadToken[ch][d] = false;
+    }
+  }  // pick baud, via top level config.h 
+  // baud bb baud
+  // 63019 for a very safe 115200
+  // 54351 for a go-karting 512000
+  // 43690 for a trotting pace of 1MHz
+  // 21845 for the E30 2MHz
+  // 0 for max-speed 3MHz
+  switch(UCBUS_BAUD){
+    case 1:
+      ub_baud_val = 43690;
+      break;
+    case 2: 
+      ub_baud_val = 21845;
+      break;
+    case 3: 
+      ub_baud_val = 0;
+      break;
+    default:
+      ub_baud_val = 43690;
+  }
+  // start the uart, 
+  setupBusHeadUART();
+  // ! alert ! need to setup timer in main.cpp 
+}
+
+void ucBusHead_timerISR(void){
+  // increment / wrap time division for drops  
+  currentDropTap ++;
+  if(currentDropTap > UB_MAX_DROPS){ // recall that tapping '0' should operate the clock reset, addr 0 doesn't exist 
+    currentDropTap = 1;
+  }
+  // reset the outgoing header, 
+  outHeader.bytes[0] = 0; 
+  outHeader.bytes[1] = 0;
+  // write in drop tap, flowcontrol rules 
+  outHeader.bits.CH0FC = (inBufferLen[0][currentDropTap] ?  0 : 1);
+  outHeader.bits.CH1FC = (inBufferLen[1][currentDropTap] ?  0 : 1);
+  outHeader.bits.DROPTAP = currentDropTap;                
+  // now we check if we can tx on either channel, 
+  for(uint8_t ch = 0; ch < UB_CH_COUNT; ch ++){
+    // do we have ahn pck to be tx'ing, and is flowcontrol condition met 
+    // FC: outBuffer[x][0] is the 'addr' we are tx'ing to, so indexes relevant rcrxb as well
+    // ! and, when we broadcast (channel '0') we ignore FC rules, so: 
+    if(outBufferLen[ch] > 0 && (rcrxb[ch][outBuffer[ch][0]] || ch == 0)){
+      // ch has incomplete-tx of some packet 
+      // count them, max we will transmit is from word length: 
+      uint8_t numTx = outBufferLen[ch] - outBufferRp[ch];
+      if(numTx > UB_DATA_BYTES_PER_WORD) numTx = UB_DATA_BYTES_PER_WORD;
+      // we can write the 2nd header byte (ch select and # of words)
+      outHeader.bits.CHSELECT = ch;
+      outHeader.bits.TOKENS = numTx;
+      // fill bytes, 
+      uint8_t *outB = outBuffer[ch];
+      uint16_t outBRp = outBufferRp[ch];
+      for(uint8_t b = 0; b < numTx; b ++){ 
+        outWord[b + 2] = outB[outBRp + b];
+      }
+      outBufferRp[ch] += numTx;
+      // if numTx < data words per packet, packet will terminate this frame, we can reset 
+      // recipient uses the tailing '0' token-d byte to delineate packets (COBS for words)
+      if(numTx < UB_DATA_BYTES_PER_WORD) {
+        // flow control: we have tx'd to whichever drop... the head recieves updates from drops 
+        // for rcrxb, but they're potentially spaced 1/64 turns of this ISR, 
+        // so we need to update our accounting of their space-available-to-receive.
+        // recall also that rcrxb is parallel per channel *and* per drop 
+        rcrxb[ch][outBuffer[ch][0]] = 0; // 0 space available here now, 
+        outBufferLen[ch] = 0; // reset also the outgoing buffer,
+        outBufferRp[ch] = 0;  // and it's read-out ptr 
+      }
+      break; // don't check the next ch, outword occupied by this 
+    }
+  }
+  // stuff header -> outWord
+  outWord[0] = outHeader.bytes[0];
+  outWord[1] = outHeader.bytes[1];
+  // insert rarechar 
+  outWord[UB_HEAD_BYTES_PER_WORD - 1] = UCBUS_RARECHAR;
+  // now we transmit: 
+  UB_SER_USART.DATA.reg = outWord[0];
+  outWordRp = 1; // next up, 
+  UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
+}
+
+// data register empty: bang next byte in 
+void SERCOM1_0_Handler(void){
+  UB_SER_USART.DATA.reg = outWord[outWordRp ++];
+  if(outWordRp >= UB_HEAD_BYTES_PER_WORD){ // if we've transmitted them all, 
+    UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE; // clear tx-data-empty interrupt 
+    UB_SER_USART.INTENSET.reg = SERCOM_USART_INTENSET_TXC; // set tx-complete interrupt 
+  }
+}
+
+// transmit complete interrupt: delimit incoming words 
+void SERCOM1_1_Handler(void){
+  UB_SER_USART.INTFLAG.bit.TXC = 1;
+  UB_SER_USART.INTENCLR.reg = SERCOM_USART_INTENCLR_TXC;
+  // this means the latest word transmit is done, next byte on the line should be 1st in 
+  // upstream pckt 
+  lastDropTap = currentDropTap;
+  inWordWp = 0;
+}
+
+// rx handler, for incoming
+void SERCOM1_2_Handler(void){
+	ucBusHead_rxISR();
+}
+
+void ucBusHead_rxISR(void){
+	// shift the byte -> incoming, 
+  inWord[inWordWp ++] = UB_SER_USART.DATA.reg;
+  if(inWordWp >= UB_DROP_BYTES_PER_WORD){
+    // that's ^ word delineation, so our drop tap should be:
+    uint8_t rxDrop = lastDropTap; 
+    // check that, 
+    inHeader.bytes[0] = inWord[0];
+    inHeader.bytes[1] = inWord[1];
+    if(inHeader.bits.DROPTAP != rxDrop){ return; } // bail on mismatch, was a bad / misaligned word
+    // update keepalive: last we heard from this drop:
+    lastRxTime[rxDrop] = millis();
+    // update our buffer states, 
+    rcrxb[0][rxDrop] = inHeader.bits.CH0FC;
+    rcrxb[1][rxDrop] = inHeader.bits.CH1FC; 
+    // the ch that drop tx'd on 
+    uint8_t rxCh = inHeader.bits.CHSELECT;
+    // has anything?
+    uint8_t numToken = inHeader.bits.TOKENS;
+    // check for broken numToken count,
+    if(numToken > UB_DATA_BYTES_PER_WORD) { 
+      OSAP::error("ucbus-head outsize numToken rx", MEDIUM); 
+      return; 
+    }
+    // if we are filling this buffer, but it's already occupied, fc has failed and we
+    if(inBufferLen[rxCh][rxDrop] != 0){ 
+      OSAP::error("ucbus-head rx FC broken", MEDIUM); 
+      return; 
+    }
+    // donot write past buffer size,
+    if(inBufferWp[rxCh][rxDrop] + numToken > UB_BUFSIZE){
+      inBufferWp[rxCh][rxDrop] = 0;
+      OSAP::error("ucbus-head rx packet too-long", MEDIUM);
+      return;
+    }
+    // shift bytes into rx buffer 
+    uint8_t * inB = inBuffer[rxCh][rxDrop];
+    uint16_t inBWp = inBufferWp[rxCh][rxDrop];
+    for(uint8_t i = 0; i < numToken; i ++){
+      inB[inBWp + i] = inWord[2 + i];
+    }
+    inBufferWp[rxCh][rxDrop] += numToken;
+    // to find packet edge, if we have numToken > numDataBytes and at least 
+    // one other in the stream, we have pckt edge
+    if(numToken > 0) lastWordHadToken[rxCh][rxDrop] = true;
+    if(numToken < UB_DATA_BYTES_PER_WORD && lastWordHadToken[rxCh][rxDrop]){
+      // packet edge, reset token edge
+      lastWordHadToken[rxCh][rxDrop] = false;
+      // pckt edge is here, set fullness, otherwise we're done, 
+      // application responsible for shifting it out and 
+      // inBufferLen is what we read to determine FC condition 
+      inBufferLen[rxCh][rxDrop] = inBufferWp[rxCh][rxDrop];
+      inBufferWp[rxCh][rxDrop] = 0;
+    }
+  }
+}
+
+// -------------------------------------------------------- API 
+
+// clear to read ? channel select ? 
+#warning TODO: bus head read per-ch: yep, should be a or b, 
+boolean ucBusHead_ctr(uint8_t drop){
+  // called once per loop, so here's where this debug goes:
+  //(rcrxb[1] > 0) ? DEBUG2PIN_OFF : DEBUG2PIN_ON; // for psu-breakout,
+  //(rcrxb[2] > 0) ? DEBUG3PIN_OFF : DEBUG3PIN_ON; // pin off is light on
+  if(drop >= UB_MAX_DROPS) return false;
+  if(inBufferLen[1][drop] > 0){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+#warning TODO: bus head osap-read-in per-ch ? currently fixed to chb osap reads 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest){
+  if(!ucBusHead_ctr(drop)) return 0;
+  size_t len = inBufferLen[1][drop];
+  memcpy(dest, inBuffer[1][drop], len);
+  __disable_irq(); // again... do we need these ? big brain time 
+  inBufferLen[1][drop] = 0;
+  inBufferWp[1][drop] = 0;
+  __enable_irq();
+  return len;
+}
+
+boolean ucBusHead_ctsA(void){
+	if(outBufferLen[0] == 0){ 
+    // only condition is that our transmit buffer is zero / are not currently tx'ing on this channel 
+		return true;
+	} else {
+		return false;
+	}
+}
+
+boolean ucBusHead_ctsB(uint8_t drop){
+  // escape states 
+  if(outBufferLen[1] == 0 && rcrxb[1][drop] > 0){
+    return true; 
+  } else {
+    return false;
+  }
+}
+
+boolean ucBusHead_isPresent(uint8_t drop){
+  if(drop > UCBUS_MAX_DROPS) return false;
+  return (millis() - lastRxTime[drop] < UB_KEEPALIVE_TIME);
+}
+
+#warning TODO: we have this awkward +1 in the buffer / segsize, vs what the app. sees... 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel){
+	if(!ucBusHead_ctsA()) return;
+  if(len > UB_BUFSIZE + 1) return; // none over buf size 
+  // 1st byte: channel ID
+  outBuffer[0][0] = channel;
+  // copy in @ 1th byte 
+  // we *shouldn't* have to guard against the memcpy, god bless, since 
+  // the bus shouldn't be touching this so long as our outBufferLen is 0,
+  // which - we are guarded against that w/ the flowcontrol check above 
+  memcpy(&(outBuffer[0][1]), data, len);
+  // len set 
+  __disable_irq();
+  outBufferLen[0] = len + 1;
+  outBufferRp[0] = 0;
+  __enable_irq();
+}
+
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop){
+  if(!ucBusHead_ctsB(drop)) return;
+  if(len > UB_BUFSIZE + 1) return; // same as above
+  __disable_irq();
+  // 1st byte: drop identifier 
+  outBuffer[1][0] = drop;
+  // copy in @ 1th byte 
+  memcpy(&(outBuffer[1][1]), data, len);
+  // length set 
+  outBufferLen[1] = len + 1; // + 1 for the addr... 
+  // read-out ptr reset 
+  outBufferRp[1] = 0;
+  __enable_irq();
+}
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..65f43edcfc482f9656fe30d0bf7f7ea0f9c1eb67
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/drivers/ucBusHead.h
+
+beginnings of a uart-based clock / bus combo protocol
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_HEAD_H_
+#define UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+
+#include <Arduino.h>
+#include "ucBusMacros.h"
+
+// setup, 
+void ucBusHead_setup(void);
+
+// need to call the main timer isr at some rate, 
+void ucBusHead_timerISR(void);
+void ucBusHead_rxISR(void);
+void ucBusHead_txISR(void);
+
+// ub interface, 
+boolean ucBusHead_ctr(uint8_t drop); // is there ahn packet to read at this drop 
+size_t ucBusHead_read(uint8_t drop, uint8_t *dest);  // get 'them bytes fam 
+//size_t ucBusHead_readPtr(uint8_t* drop, uint8_t** dest, unsigned long *pat); // vport interface, get next to handle... 
+//void ucBusHead_clearPtr(uint8_t drop);
+boolean ucBusHead_ctsA(void);  // return true if TX complete / buffer ready
+boolean ucBusHead_ctsB(uint8_t drop);
+boolean ucBusHead_isPresent(uint8_t drop); // have we heard from this drop recently ? 
+void ucBusHead_transmitA(uint8_t *data, uint16_t len, uint8_t channel);  // ship bytes: broadcast to all 
+void ucBusHead_transmitB(uint8_t *data, uint16_t len, uint8_t drop);  // ship bytes: 0-14: individual drop, 15: broadcast
+
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusMacros.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusMacros.h
new file mode 100644
index 0000000000000000000000000000000000000000..72f3f0c0b60e6c7efac390db2e6db4be7e9b133a
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucBusMacros.h
@@ -0,0 +1,127 @@
+/*
+ucBusMacros.h
+
+config / utes for the uart-clocked bus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+
+#ifndef UCBUS_MACROS_H_
+#define UCBUS_MACROS_H_
+
+#include "./ucbus_config.h"
+#include <Arduino.h>
+
+// ---------------------------------------------- INFO 
+
+/*
+    assuming for now there is one bus PHY per micro, 
+    this is for shared hardware config *and* macros to operate 
+    / read / write on the bus 
+*/
+
+// ---------------------------------------------- BUFFER / DROP SIZES / RATES
+// the channel count: 2
+#define UB_CH_COUNT 2 
+// the size of each buffer: also the maximum segment size 
+#define UB_BUFSIZE 256
+// time-until-considered-dead, in ms  
+#define UB_KEEPALIVE_TIME 200 
+// max. # of drops on the bus, just swapping from top level config.h 
+#define UB_MAX_DROPS UCBUS_MAX_DROPS
+// with a fixed 2-byte header, we can have some max # of data bytes, 
+// this is *probably* going to stay at 10, but might fluxuate a little 
+#define UB_DATA_BYTES_PER_WORD 12
+#define UB_HEAD_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 3)     // + 2 header, + 1 rare character
+#define UB_DROP_BYTES_PER_WORD (UB_DATA_BYTES_PER_WORD + 2)     // + 2 header
+
+// ---------------------------------------------- DATA WORDS -> INFO 
+
+typedef union {
+    struct {
+        uint8_t CH0FC:1;    // bit: channel 0 reported flowcontrol (1: full, 0: cts)
+        uint8_t CH1FC:1;    // bit: channel 1 reported flowcontrol 
+        uint8_t DROPTAP:6;  // 0-63: time division drop 
+        uint8_t CHSELECT:1; // bit: channel select: 1 for ch1, 0 ch0
+        uint8_t RESERVED:3; // not currently used, 
+        uint8_t TOKENS:4;   // 0-15: how many bytes in word are real data bytes 
+    } bits;
+    uint8_t bytes[2];
+} UCBUS_HEADER_Type;
+
+#define UCBUS_RARECHAR 0b10101010
+
+// ---------------------------------------------- PORT / PIN CONFIGS 
+#ifdef UCBUS_IS_D51
+// ------------------------------------ D51 HAL
+#define UB_SER_USART SERCOM1->USART
+#define UB_SERCOM_CLK SERCOM1_GCLK_ID_CORE
+#define UB_GCLKNUM_PICK 7
+#define UB_COMPORT PORT->Group[0]
+#define UB_TXPIN 16  // x-0
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 18  // x-2
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 2 // RX on SER-2
+#define UB_TXPERIPHERAL 2 // A: 0, B: 1, C: 2
+#define UB_RXPERIPHERAL 2
+
+// the data enable / reciever enable pins were modified between module circuit 
+// revisions: the board w/ an SMT JTAG header is "the OG" module, 
+// these are from board-level config
+#ifdef IS_OG_MODULE 
+#define UB_DE_PIN 16 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[1] 
+#define UB_RE_PIN 19 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[0]
+#else 
+#define UB_DE_PIN 19 // driver output enable: set HI to enable, LO to tri-state the driver 
+#define UB_DE_PORT PORT->Group[0] 
+#define UB_RE_PIN 9 // receiver output enable, set LO to enable the RO, set HI to tri-state RO 
+#define UB_RE_PORT PORT->Group[1]
+#endif 
+
+#define UB_TE_PIN 17  // termination enable, drive LO to enable to internal termination resistor, HI to disable
+#define UB_TE_PORT PORT->Group[0]
+#define UB_TE_BM (uint32_t)(1 << UB_TE_PIN)
+#define UB_RE_BM (uint32_t)(1 << UB_RE_PIN)
+#define UB_DE_BM (uint32_t)(1 << UB_DE_PIN)
+
+#define UB_DRIVER_ENABLE UB_DE_PORT.OUTSET.reg = UB_DE_BM
+#define UB_DRIVER_DISABLE UB_DE_PORT.OUTCLR.reg = UB_DE_BM
+// ------------------------------------ END D51 HAL 
+#endif 
+
+#ifdef UCBUS_IS_D21
+// ------------------------------------ D21 HAL 
+#define UB_SER_USART SERCOM1->USART 
+#define UB_PORT PORT->Group[0]
+#define UB_TXPIN 16
+#define UB_TXBM (uint32_t)(1 << UB_TXPIN)
+#define UB_RXPIN 19
+#define UB_RXBM (uint32_t)(1 << UB_RXPIN)
+#define UB_RXPO 3 // RX is on SER1-3
+#define UB_TXPERIPHERAL PERIPHERAL_C
+#define UB_RXPERIPHERAL PERIPHERAL_C
+// data enable, recieve enable pins 
+#define UB_DEPIN 17
+#define UB_DEBM (uint32_t)(1 << UB_DEPIN)
+#define UB_REPIN 18
+#define UB_REBM (uint32_t)(1 << UB_REPIN)
+#define UB_DRIVER_ENABLE UB_PORT.OUTSET.reg = UB_DEBM
+#define UB_DRIVER_DISABLE UB_PORT.OUTCLR.reg = UB_DEBM
+#define UB_DE_SETUP UB_PORT.DIRSET.reg = UB_DEBM; UB_DRIVER_DISABLE
+#define UB_RECIEVE_ENABLE UB_PORT.OUTCLR.reg = UB_REBM
+#define UB_RECIEVE_DISABLE UB_PORT.OUTSET.reg = UB_REBM
+#define UB_RE_SETUP UB_PORT.DIRSET.reg = UB_REBM; UB_RECIEVE_ENABLE
+// ------------------------------------ END D21 HAL 
+#endif 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.cpp b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08742fdc5cda9435f8ad54b76a4f85c81c433b1e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.cpp
@@ -0,0 +1,61 @@
+// DIPs
+#include "ucBusDipConfig.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+void dip_setup(void){
+    // set direction in,
+    DIP_PORT.DIRCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+    // enable in,
+    DIP_PORT.PINCFG[D0_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.INEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.INEN = 1;
+    // enable pull,
+    DIP_PORT.PINCFG[D0_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D1_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D2_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D3_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D4_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D5_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D6_PIN].bit.PULLEN = 1;
+    DIP_PORT.PINCFG[D7_PIN].bit.PULLEN = 1;
+    // 'pull' references the value set in the 'out' register, so to pulldown:
+    DIP_PORT.OUTCLR.reg = D_BM(D0_PIN) | D_BM(D1_PIN) | D_BM(D2_PIN) | D_BM(D3_PIN) | D_BM(D4_PIN) | D_BM(D5_PIN) | D_BM(D6_PIN) | D_BM(D7_PIN);
+}
+
+uint8_t dip_readLowerFive(void){
+    uint32_t bits[5] = {0,0,0,0,0};
+    if(DIP_PORT.IN.reg & D_BM(D7_PIN)) { bits[0] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D6_PIN)) { bits[1] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D5_PIN)) { bits[2] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D4_PIN)) { bits[3] = 1; }
+    if(DIP_PORT.IN.reg & D_BM(D3_PIN)) { bits[4] = 1; }
+    /*
+    bits[0] = (DIP_PORT.IN.reg & D_BM(D7_PIN)) >> D7_PIN;
+    bits[1] = (DIP_PORT.IN.reg & D_BM(D6_PIN)) >> D6_PIN;
+    bits[2] = (DIP_PORT.IN.reg & D_BM(D5_PIN)) >> D5_PIN;
+    bits[3] = (DIP_PORT.IN.reg & D_BM(D4_PIN)) >> D4_PIN;
+    bits[4] = (DIP_PORT.IN.reg & D_BM(D3_PIN)) >> D3_PIN;
+    */
+    // not sure why I wrote this as uint32 (?) 
+    uint32_t word = 0;
+    word = word | (bits[4] << 4) | (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | (bits[0] << 0);
+    return (uint8_t)word;
+}
+
+boolean dip_readPin0(void){
+    return DIP_PORT.IN.reg & D_BM(D0_PIN);
+}
+
+boolean dip_readPin1(void){
+    return DIP_PORT.IN.reg & D_BM(D1_PIN);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..97ec2b5750e86bbd6d98acd3ef42c02b489240f4
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/ucbusDipConfig.h
@@ -0,0 +1,36 @@
+// DIP switch HAL macros 
+// pardon the mis-labeling: on board, and in the schem, these are 1-8, 
+// here they will be 0-7 
+
+// note: these are 'on' hi by default, from the factory. 
+// to set low, need to turn the internal pulldown on 
+
+#ifndef UCBUS_DIP_CONFIG_H_
+#define UCBUS_DIP_CONFIG_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_D51 
+#ifdef UCBUS_IS_DROP
+
+#include <Arduino.h>
+
+#define D0_PIN 5
+#define D1_PIN 4
+#define D2_PIN 3
+#define D3_PIN 2
+#define D4_PIN 1 
+#define D5_PIN 0
+#define D6_PIN 31 
+#define D7_PIN 30
+#define DIP_PORT PORT->Group[1]
+#define D_BM(val) ((uint32_t)(1 << val))
+
+void dip_setup(void);
+uint8_t dip_readLowerFive(void);  // id, five bits, 0: clock reset, 1:31: drop ids, 
+boolean dip_readPin0(void); // bus-head (hi) or bus-drop (lo) (not used: firmware config drop or head) 
+boolean dip_readPin1(void); // if bus-drop, te-enable (hi) or no (lo)
+
+#endif 
+#endif
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.cpp b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a49901b40751839e972e4f5d778179834dba1868
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.cpp
@@ -0,0 +1,95 @@
+/*
+osap/vport_ucbus_drop.cpp
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusDrop.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusDrop.h"
+#include "../osape/core/osap.h"
+
+// badness, direct write in future 
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusDrop::VBus_UCBusDrop(Vertex* _parent, String _name
+): VBus(_parent, _name){
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusDrop::begin(void){
+  ucBusDrop_setup(true, 0);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::begin(uint8_t _ownRxAddr){
+  ucBusDrop_setup(false, _ownRxAddr);
+  ownRxAddr = ucBusDrop_getOwnID();
+}
+
+void VBus_UCBusDrop::loop(void){
+  // can we shift-in from channel a / broadcast messages ?
+  // also... stack 'em from the broadcast channel first, typically higher priority 
+  if(ucBusDrop_ctrA()){
+    // and if we have an empty space... 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+    // get len & strip out the broadcastChannel, which was stuffed at [0]
+    uint16_t len = ucBusDrop_readA(_tempBuffer);
+    injestBroadcastPacket(&(_tempBuffer[1]), len - 1, _tempBuffer[0]);
+    }
+  }
+  // can we shift-in from channel b / directed messages ? 
+  if(ucBusDrop_ctrB()){
+    // find a slot, 
+    if(stackEmptySlot(this, VT_STACK_ORIGIN)){
+      // copy in to origin stack 
+      uint16_t len = ucBusDrop_readB(_tempBuffer);
+      stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+    } else {
+      // no empty space, will wait in bus 
+    }
+  }
+}
+
+void VBus_UCBusDrop::send(uint8_t* data, uint16_t len, uint8_t rxAddr){
+  // can't tx not-to-the-head, will drop pck 
+  if(rxAddr != 0) return;
+  // if the bus is ready, drop it,
+  if(ucBusDrop_ctsB()){
+    ucBusDrop_transmitB(data, len);
+  } else {
+    OSAP::error("ubd tx while not clear", MEDIUM);
+  }
+}
+
+boolean VBus_UCBusDrop::cts(uint8_t rxAddr){
+  // immediately clear? & transmit only to head 
+  return (rxAddr == 0 && ucBusDrop_ctsB());
+}
+
+void VBus_UCBusDrop::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  OSAP::debug("Broadcast is unwritten");
+}
+
+boolean VBus_UCBusDrop::ctb(uint8_t broadcastChannel){
+  OSAP::debug("Bus Drop CTB is unwritten");
+  return false;
+}
+
+boolean VBus_UCBusDrop::isOpen(uint8_t rxAddr){
+  return ucBusDrop_isPresent(rxAddr);
+}
+
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4333e6491b0439d01ae4bc480bce37af864f5
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusDrop.h
@@ -0,0 +1,41 @@
+/*
+osap/vport_ucbus_drop.h
+
+virtual port, bus drop, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VBUS_UCBUS_HEAD_H_
+#define VBUS_UCBUS_HEAD_H_
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_DROP
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusDrop : public VBus {
+  public:
+    void begin(void);
+    void begin(uint8_t _ownRxAddr);
+    void loop(void) override;
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr);
+    VBus_UCBusDrop(Vertex* _parent, String _name);
+};
+
+#endif 
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.cpp b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0e5cd5676e138fa0c17215c075181594ff48ac
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.cpp
@@ -0,0 +1,93 @@
+/*
+osap/vb_ucBusHead.cpp
+
+virtual port, bus head / host
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#include "vb_ucBusHead.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include "ucBusHead.h"
+#include "../osape/core/osap.h"
+
+// locally, track which drop we shifted in a packet from last
+uint8_t _lastDropHandled = 0;
+
+// badness, should remove w/ direct copy in API eventually
+uint8_t _tempBuffer[UB_BUFSIZE];
+
+VBus_UCBusHead::VBus_UCBusHead(Vertex* _parent, String _name
+): VBus (_parent, _name) {
+  // report our address size,
+  addrSpaceSize = UCBUS_MAX_DROPS;
+}
+
+void VBus_UCBusHead::begin(void){
+  // start ucbus
+  ucBusHead_setup(); 
+}
+
+void VBus_UCBusHead::loop(void){
+  // we need to shift items from the bus into the origin stack here
+  // we can shift multiple in per turn, if stack space exists
+  uint8_t drop = _lastDropHandled;
+  for (uint8_t i = 1; i < UB_MAX_DROPS; i++) {
+    drop++;
+    if (drop >= UB_MAX_DROPS) {
+      drop = 1;
+    }
+    if (ucBusHead_ctr(drop)) {
+      // find a stack slot,
+      if (stackEmptySlot(this, VT_STACK_ORIGIN)) {
+        // copy it in, 
+        uint16_t len = ucBusHead_read(drop, _tempBuffer);
+        stackLoadSlot(this, VT_STACK_ORIGIN, _tempBuffer, len);
+      } else {
+        // no more empty spaces this turn, continue 
+        return; 
+      }
+    }
+  }
+}
+
+void VBus_UCBusHead::timerISR(void){
+  ucBusHead_timerISR();
+}
+
+void VBus_UCBusHead::send(uint8_t* data, uint16_t len, uint8_t rxAddr) {
+  if (rxAddr == 0) {
+    OSAP::error("attempt to busf from head to self", MEDIUM);
+  } else {  
+    ucBusHead_transmitB(data, len, rxAddr);
+  }
+}
+
+boolean VBus_UCBusHead::cts(uint8_t rxAddr){
+  // mapping rxAddr in osap space (where 0 is head) to ucbus drop-id space...
+  return ucBusHead_ctsB(rxAddr);
+}
+
+void VBus_UCBusHead::broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel){
+  ucBusHead_transmitA(data, len, broadcastChannel);
+}
+
+boolean VBus_UCBusHead::ctb(uint8_t broadcastChannel){
+  return ucBusHead_ctsA();
+}
+
+boolean VBus_UCBusHead::isOpen(uint8_t rxAddr){
+  return ucBusHead_isPresent(rxAddr);
+}
+
+#endif 
+#endif
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.h b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfb7829f135f8ea04f193d3657f38cb15ea63cfa
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/osape_ucbus/vb_ucBusHead.h
@@ -0,0 +1,45 @@
+/*
+osap/vb_ucBusHead.h
+
+virtual port, bus head, ucbus 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef VPORT_UCBUS_HEAD_H_
+#define VPORT_UCBUS_HEAD_H_ 
+
+#include "./ucbus_config.h"
+
+#ifdef UCBUS_IS_HEAD
+#ifdef UCBUS_ON_OSAP 
+
+#include <Arduino.h>
+#include "../osape/core/vertex.h"
+
+class VBus_UCBusHead : public VBus {
+  public:
+    void begin(void);
+    // loop to ferry data, 
+    void loop(void) override;
+    // fast loop, needs to be called in ~ 10kHz ISR 
+    void timerISR(void);
+    // ... bus : osap API 
+    void send(uint8_t* data, uint16_t len, uint8_t rxAddr) override;
+    void broadcast(uint8_t* data, uint16_t len, uint8_t broadcastChannel) override;
+    boolean cts(uint8_t rxAddr) override;
+    boolean ctb(uint8_t broadcastChannel) override;
+    boolean isOpen(uint8_t rxAddr) override;
+    // -------------------------------- Constructors 
+    VBus_UCBusHead(Vertex* _parent, String _name);
+};
+
+#endif
+#endif 
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/ucbus_config.h b/system/firmware/lpf-modular-motion-head/src/ucbus_config.h
new file mode 100644
index 0000000000000000000000000000000000000000..6744c905cd89421c5b9f0d4043b8da8d0f14b3cb
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/ucbus_config.h
@@ -0,0 +1,29 @@
+/*
+ucbus_confi.h
+
+config options for an ucbus instance 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the osap project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+#ifndef UCBUS_CONFIG_H_
+#define UCBUS_CONFIG_H_
+
+#define UCBUS_MAX_DROPS 16
+//#define UCBUS_IS_DROP 
+#define UCBUS_IS_HEAD 
+
+#define UCBUS_BAUD 2 
+
+#define UCBUS_IS_D51
+// #define UCBUS_IS_D21
+
+#define UCBUS_ON_OSAP 
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/utils_samd51/README.md b/system/firmware/lpf-modular-motion-head/src/utils_samd51/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a5e4922e5be8001ad756c57dc6cd5c934ca1572e
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/utils_samd51/README.md
@@ -0,0 +1,3 @@
+## ATSAMD51 Utes
+
+`learning-polymer-flows` branch 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.cpp b/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387cbaad46e7f17a5f553c446a47558abbc20bc7
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.cpp
@@ -0,0 +1,129 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#include "clock_utils.h"
+#include "../indicators.h"
+
+/*
+// I used to have this singleton stuff here, but I think
+// since I am using the extern... I have no need for it, 
+D51ClockUtils* D51ClockUtils::instance = 0;
+
+D51ClockUtils* D51ClockUtils::getInstance(void){
+    if(instance == 0){
+        instance = new D51ClockUtils();
+    }
+    return instance;
+}
+
+D51ClockUtils* D51ClockUtils = D51ClockUtils::getInstance();
+*/
+
+D51ClockUtils* d51ClockUtils;
+
+D51ClockUtils::D51ClockUtils(){}
+
+void D51ClockUtils::setup_16mhz_xtal(void){
+    if(mhz_xtal_is_setup) return; // already done, 
+    // let's make a clock w/ that xtal:
+    OSCCTRL->XOSCCTRL[0].bit.RUNSTDBY = 0;
+    OSCCTRL->XOSCCTRL[0].bit.XTALEN = 1;
+    // set oscillator current..
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_IMULT(4) | OSCCTRL_XOSCCTRL_IPTAT(3);
+    OSCCTRL->XOSCCTRL[0].reg |= OSCCTRL_XOSCCTRL_STARTUP(5);
+    OSCCTRL->XOSCCTRL[0].bit.ENALC = 1;
+    OSCCTRL->XOSCCTRL[0].bit.ENABLE = 1;
+    // make the peripheral clock available on this ch 
+    GCLK->GENCTRL[MHZ_XTAL_GCLK_NUM].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_XOSC0) | GCLK_GENCTRL_GENEN;  // GCLK_GENCTRL_SRC_DFLL
+    while (GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL(MHZ_XTAL_GCLK_NUM)){
+        //DEBUG2PIN_TOGGLE;
+    };
+    mhz_xtal_is_setup = true;
+}
+
+void D51ClockUtils::start_ticker_a(uint32_t us){
+    //now using 120mHz main clock (gen(0)) instead of xtal, 
+    //setup_16mhz_xtal();
+    // ok
+    TC0->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC1->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0 | MCLK_APBAMASK_TC1;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC0_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(0);//this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC0->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC1->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC0->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC1->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC0->COUNT32.INTENSET.bit.MC0 = 1;
+    TC0->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC0->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 100kHz, ok? 
+    if(us < 10) us = 10;
+    // 120 / 2 -> 60 ticks per us, 
+    TC0->COUNT32.CC[0].reg = 60 * us;
+    // enable, sync for enable write
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC0->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC0->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC1->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC0_IRQn);
+    NVIC_SetPriority(TC0_IRQn, 2);
+}
+
+void D51ClockUtils::set_ticker_a_priority(uint32_t prio){
+    if(prio > 3) prio = 3;
+    NVIC_SetPriority(TC0_IRQn, prio);
+}
+
+void D51ClockUtils::start_ticker_b(uint32_t us){
+    setup_16mhz_xtal();
+    // ok
+    TC2->COUNT32.CTRLA.bit.ENABLE = 0;
+    TC3->COUNT32.CTRLA.bit.ENABLE = 0;
+    // unmask clocks
+    MCLK->APBBMASK.reg |= MCLK_APBBMASK_TC2 | MCLK_APBBMASK_TC3;
+    // ok, clock to these channels...
+    GCLK->PCHCTRL[TC2_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    GCLK->PCHCTRL[TC3_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN(this->mhz_xtal_gclk_num);
+    // turn them ooon...
+    TC2->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    TC3->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_PRESCALER_DIV2 | TC_CTRLA_CAPTEN0;
+    // going to set this up to count at some time, we will tune
+    // that freq. with
+    TC2->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    TC3->COUNT32.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
+    // allow interrupt to trigger on this event (overflow)
+    TC2->COUNT32.INTENSET.bit.MC0 = 1;
+    TC2->COUNT32.INTENSET.bit.MC1 = 1;
+    // set the period,
+    while (TC2->COUNT32.SYNCBUSY.bit.CC0);
+    // 8 counts in here per us
+    // nothing > 1MHz, ok? 
+    if(us < 8) us = 8;
+    TC2->COUNT32.CC[0].reg = 8 * us;
+    // enable, sync for enable write
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC2->COUNT32.CTRLA.bit.ENABLE = 1;
+    while (TC2->COUNT32.SYNCBUSY.bit.ENABLE);
+    TC3->COUNT32.CTRLA.bit.ENABLE = 1;
+    // enable the IRQ
+    NVIC_EnableIRQ(TC2_IRQn);
+}
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.h b/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1f9e7472c034dc3a1aee6fc995eb65043d660
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/utils_samd51/clock_utils.h
@@ -0,0 +1,45 @@
+/*
+utils_samd51/clock_utils.h
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and ponyo
+projects. Copyright is retained and must be preserved. The work is provided as
+is; no warranty is provided, and users accept all liability.
+*/
+
+#ifndef CLOCKS_D51_H_
+#define CLOCKS_D51_H_
+
+#include <Arduino.h>
+
+#define MHZ_XTAL_GCLK_NUM 9
+
+class D51ClockUtils {
+    private:
+        static D51ClockUtils* instance;
+    public:
+        D51ClockUtils();
+        static D51ClockUtils* getInstance(void);
+        // xtal
+        volatile boolean mhz_xtal_is_setup = false;
+        uint32_t mhz_xtal_gclk_num = 9;
+        void setup_16mhz_xtal(void);
+        // uses TC0 and TC1 as 32 bit TC
+        // pickup TC0_Handler(void){}
+        // do in handler: 
+        // TC0->COUNT32.INTFLAG.bit.MC0 = 1;
+        // TC0->COUNT32.INTFLAG.bit.MC1 = 1;
+        // us: requested timer period 
+        void start_ticker_a(uint32_t us);
+        void set_ticker_a_priority(uint32_t prio);
+        // uses TC2 and TC3 as 32 bit TC 
+        // pickup on TC2_Handler(void){}
+        void start_ticker_b(uint32_t us);
+};
+
+extern D51ClockUtils* d51ClockUtils;
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/utils_samd51/peripheral_nums.h b/system/firmware/lpf-modular-motion-head/src/utils_samd51/peripheral_nums.h
new file mode 100644
index 0000000000000000000000000000000000000000..eed9f188afacfb0da271d43603f833f61ec61191
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/utils_samd51/peripheral_nums.h
@@ -0,0 +1,18 @@
+#ifndef PERIPHERAL_NUMS_H_
+#define PERIPHERAL_NUMS_H_
+
+#define PERIPHERAL_A 0
+#define PERIPHERAL_B 1
+#define PERIPHERAL_C 2
+#define PERIPHERAL_D 3
+#define PERIPHERAL_E 4
+#define PERIPHERAL_F 5
+#define PERIPHERAL_G 6
+#define PERIPHERAL_H 7
+#define PERIPHERAL_I 8
+#define PERIPHERAL_K 9
+#define PERIPHERAL_L 10
+#define PERIPHERAL_M 11
+#define PERIPHERAL_N 12
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/src/utils_samd51/pin_macros.h b/system/firmware/lpf-modular-motion-head/src/utils_samd51/pin_macros.h
new file mode 100644
index 0000000000000000000000000000000000000000..89418657d8a481cb20ec3532cbd3ef0488dda521
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/src/utils_samd51/pin_macros.h
@@ -0,0 +1,13 @@
+#ifndef PIN_MACROS_D51_H_
+#define PIN_MACROS_D51_H_
+
+#define PIN_BM(pin) (uint32_t)(1 << pin)
+#define PIN_HI(port, pin) PORT->Group[port].OUTSET.reg = PIN_BM(pin) 
+#define PIN_LO(port, pin) PORT->Group[port].OUTCLR.reg = PIN_BM(pin) 
+#define PIN_TGL(port, pin) PORT->Group[port].OUTTGL.reg = PIN_BM(pin)
+#define PIN_SETUP_OUTPUT(port, pin) PORT->Group[port].DIRSET.reg = PIN_BM(pin) 
+#define PIN_SETUP_INPUT(port, pin) PORT->Group[port].DIRCLR.reg = PIN_BM(pin); PORT->Group[port].PINCFG[pin].reg = PORT_PINCFG_INEN
+#define PIN_SETUP_PULLEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PULLEN = 1
+#define PIN_SETUP_PMUXEN(port, pin) PORT->Group[port].PINCFG[pin].bit.PMUXEN = 1
+
+#endif 
\ No newline at end of file
diff --git a/system/firmware/lpf-modular-motion-head/test/README b/system/firmware/lpf-modular-motion-head/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..b94d0890faa00a63737892509a5ca77ad3bdc6c3
--- /dev/null
+++ b/system/firmware/lpf-modular-motion-head/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Unit Testing and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/page/plus/unit-testing.html
diff --git a/system/javascript/client/flowMaps.js b/system/javascript/client/flowMaps.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7a2f9182c085a74019c43120592b586da8915ef
--- /dev/null
+++ b/system/javascript/client/flowMaps.js
@@ -0,0 +1,889 @@
+/*
+controllerClient.js
+
+basics, to fork 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+// core elements 
+import OSAP from '../osapjs/core/osap.js'
+import TIME from '../osapjs/core/time.js'
+import PK from '../osapjs/core/packets.js'
+
+// virtual machines 
+import PowerSwitchVM from '../osapjs/vms/powerSwitches.js'
+import AXLActuator from '../osapjs/vms/axlActuator.js'
+import TempVM from '../osapjs/vms/tempVirtualMachine.js'
+import LoadVM from '../osapjs/vms/loadcellVirtualMachine.js'
+import FilamentSensorVM from '../osapjs/vms/filamentSensorVirtualMachine.js'
+
+import { SaveFile } from '../osapjs/client/utes/saveFile.js'
+
+// ui elements 
+import Grid from '../osapjs/client/interface/grid.js' // main drawing API 
+import { Button, EZButton, TextBlock, TextInput } from '../osapjs/client/interface/basics.js'
+import TempPanel from '../osapjs/client/components/tempPanel.js'
+import AutoPlot from '../osapjs/client/components/autoPlot.js'
+import AXLCore from '../osapjs/vms/axlCore.js'
+
+console.log(`------------------------------------------`)
+console.log("hello lpf controller")
+
+// -------------------------------------------------------- The main UI... thing 
+
+let grid = new Grid()
+
+// -------------------------------------------------------- OSAP Object
+let osap = new OSAP("flow-mapper")
+
+// -------------------------------------------------------- SETUP NETWORK / PORT 
+let wscVPort = osap.vPort("wscVPort")
+
+// -------------------------------------------------------- Ute to switch system power at the motion-head circuit
+
+// this one is built "the new way" 
+// the psu-head / modular-motion-head board: which is just a bus router and power switches to us, 
+let powerSwitchVM = new PowerSwitchVM(osap)
+
+// these are older code, we want global vars for but can't constructor them until we have a route:
+// the extruder motor 
+let extruderMVM = {}
+let effectiveDriveGearDiameter = 8.2 * 0.9
+// temperature / heater module 
+let tempVM = {}
+// filament sensor 
+let fsVM = {}
+// load sensor, 
+let loadVM = {}
+
+// -------------------------------------------------------- Setup Code 
+
+let setup = async () => {
+  try {
+    console.warn(`SETUP: finding switches...`)
+    await powerSwitchVM.setup()
+    console.warn(`SETUP: cycling power...`)
+    await powerSwitchVM.setPowerStates(false, false)
+    await TIME.delay(500)
+    await powerSwitchVM.setPowerStates(true, false)
+    await TIME.delay(250)
+    await powerSwitchVM.setPowerStates(true, true)
+    await TIME.delay(2500)
+    // start a-lookin, 
+    console.warn(`SETUP: collecting graph...`)
+    let graph = await osap.nr.sweep()
+    // collect and setup the extruder;
+    console.warn(`SETUP: collecting extruder motor...`)
+    extruderMVM = new AXLActuator(
+      osap,
+      PK.VC2VMRoute((await osap.nr.find("rt_axl-stepper_e", graph)).route),
+      {
+        name: "rt_axl-stepper_e",
+        accelLimits: [1000, 1000, 1000, 1000], // should become a 4dof axl instance, derp... 
+        velocityLimits: [100, 100, 100, 100],
+        queueStartDelay: 500,
+        actuatorID: 0,
+        axis: 0,
+        invert: false,
+        microstep: 16,
+        spu: (16 * 200) / (effectiveDriveGearDiameter * Math.PI),
+        cscale: 0.55
+      })
+    await extruderMVM.setup()
+    // collect and setup tempvm, 
+    console.warn(`SETUP: collecting heater module...`)
+    tempVM = new TempVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_heater-module", graph)).route))
+    console.warn(`SETUP: collecting filament sensor...`)
+    fsVM = new FilamentSensorVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_filament-sensor", graph)).route))
+    console.warn(`SETUP: collecting loadcells...`)
+    loadVM = new LoadVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_loadcell-amp", graph)).route))
+    console.warn(`SETUP: DONE`)
+    // comment / uncomment these three lines to run the experiment or just plot (to watch temps drop for safe power down)
+    // runLoop = true 
+    // plotLoop()
+    // return 
+    // ------------------------------ here's a new experiment: the spin-down, 
+    // let temp = 290            // startup temp, 
+    // let rates = [
+    //   2, 5, 10, 15, 20 
+    // ]
+    // ... I would avoid rates below 10mm^3/sec for the 0.8mm shnozz: 
+    // esp. where filaments are of lower quality / lower diameter, it tends to happen 
+    // that as pressure increases, filament seeps back up around the cold stuff and clogs 
+    // the heat brake... 
+    // and I find most bonks happen around 700k loadcell reading... 
+    // add 25, rm "2" for 0.8mm noz ? 
+    await runSpinDownExperiment({
+      temp: 290, 
+      rate: 5,
+      count: 2,
+      materialName: "biopetg",
+      nozSize: "04",
+    })
+    return 
+    // ------------------------------ below is ye-olden experiment, 
+    // here's the experiment; 
+    // OK we're ready to restart w/ new sensor fw ... 
+    let tempSamples = []
+    let temp = 160
+    // ... the await temps, laggy 
+    // ... the early temps, we're still not sure about 
+    while (temp <= 290) {
+      // start each temp at rate = 1 
+      let rate = 1
+      // await temp setup here, 
+      // first we would want to set a temp and wait for it,
+      console.warn(`LOOP: set / await temp ${temp} ...`)
+      runLoop = true
+      plotLoop()
+      await tempVM.setExtruderTemp(temp)
+      // let's await like this... 
+      await tempVM.awaitExtruderTemp(temp)
+      // we should purge at our new temp, since last cycle likely shredded some filament 
+      let dpt = await getDataPoint(temp, 0.5, 1000)
+      // now we can collect at these temps, 
+      let rateSamples = []
+      while (rate <= 15) {
+        // carry on... 
+        let dpt = await getDataPoint(temp, rate, 2500)
+        if ((dpt.rate / dpt.requestedRate) < 0.80 || dpt.broken) {
+          rateSamples.push(dpt)
+          console.log(`LOOP: breaking for slip at t: ${temp}, rate: ${rate}`)
+          break;
+        } else {
+          rateSamples.push(dpt)
+          rate += 1
+        }
+        // await TIME.delay(2000)
+        // if((dpt.rate / dpt.requestedRate) < 0.80){
+        //   console.warn(`LOOP: breaking at temp ${temp} and rate ${rate} for under-extrusion`)
+        //   // pick a new rate, just knock it down 3 ticks, 
+        //   rate -= 3
+        //   if(rate < 1) rate = 1
+        //   console.warn(`LOOP: new rate for ${rate}`)
+        //   break
+        // }
+        // rate += 1 
+        // if(rate < 5){
+        //   rate += 1
+        // } else {
+        //   rate += 3
+        // }
+      } // end while-rates, 
+      tempSamples.push(rateSamples)
+      temp += 5
+      // if(temp < 210){
+      //   temp += 5
+      // } else {
+      //   temp += 20 
+      // }
+    } // end while-temps, 
+    // for(let temp = 180; temp <= 260; temp += 10){
+    //   let rateSamples = []
+    //   for(let rate = 1; rate <= 15; rate += 2){
+    //     let dpt = await getDataPoint(temp, rate, 10000)
+    //     rateSamples.push(dpt)
+    //     await TIME.delay(2000)
+    //     if((dpt.rate / dpt.requestedRate) < 0.75){
+    //       console.warn(`LOOP: breaking at ${rate} for under-extrusion`)
+    //       break
+    //     }
+    //   }
+    //   tempSamples.push(rateSamples)
+    // }
+    SaveFile(tempSamples, 'json', 'test-trials')
+    console.log(`------------------------------------------`)
+    await run()
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+let spinStop = false 
+
+let stopSpinDownButton = new EZButton(10, 10, 100, 100, `halt spindown`)
+stopSpinDownButton.onClick(() => {
+  spinStop = true 
+})
+
+let runSpinDownExperiment = async (params) => {
+  try {
+    let temp = params.temp 
+    // convert local rate (linear) from spec'd cubic mm/sec 
+    let flowRate = params.rate
+    let rate = flowRate / (((1.75/2) * (1.75/2)) * Math.PI)
+    let count = params.count 
+    let materialName = params.materialName
+    let nozSize = params.nozSize 
+    // do for two counts, 
+    outerloop: for(let c = 0; c < count; c ++){
+      console.warn(`SD: set / await ${temp} degs C to test rate ${flowRate} for ${materialName} with ${nozSize} dia`)
+      runLoop = true 
+      plotLoop()
+      await tempVM.setExtruderTemp(temp)
+      await tempVM.awaitExtruderTemp(temp)
+      // let's purge for some time, 
+      console.warn(`SD: purge...`)
+      await extruderMVM.gotoVelocity([0.5, 0, 0, 0])
+      await TIME.delay(15000)
+      // now we setup the cooldown-and-run, 
+      await extruderMVM.gotoVelocity([rate, 0, 0, 0])
+      await TIME.delay(2000)
+      await tempVM.setExtruderTemp(0)
+      // marks, 
+      let startTime = TIME.getTimeStamp()
+      let runSamples = []
+      // and gather-while, 
+      let slipEstimate = 1 
+      let slipAlpha = 0.15
+      innerLoop: while(true){
+        if(spinStop){
+          spinStop = false 
+          stopSpinDownButton.good()
+          break innerLoop;
+        }
+        let stat = await gather()
+        stat.requestedRate = rate 
+        stat.time = TIME.getTimeStamp() - startTime 
+        let slip = stat.rate / rate 
+        runSamples.push(stat)
+        slipEstimate = slipEstimate * (1 - slipAlpha) + slip * slipAlpha
+        console.warn(`slip ${slip.toFixed(2)} : ${slipEstimate.toFixed(2)} : ${stat.time}`)
+        await TIME.delay(100)
+        if(slipEstimate < 0.75) break innerLoop;
+      }
+      // stap ! 
+      await extruderMVM.gotoVelocity([0,0,0,0])
+      // check runtime 
+      let runTime = TIME.getTimeStamp() - startTime
+      console.warn(`SD: done rate-run... was ${(runTime / 1000).toFixed(2)} secs`)
+      SaveFile(runSamples, 'json', `spindown-${materialName}-${nozSize}-${flowRate.toFixed(2)}`)
+    } // end while rate < maxRate 
+    console.warn(`done parameter set...`)
+    console.warn(`------------------------------------------`)
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+setTimeout(setup, 750)
+
+// -------------------------------------------------------- UI Elements 
+
+// -------------------------------------------------------- Temp Panel / Etc 
+
+let tPanel = new AutoPlot(130, 10, 650, 230, 'hotend',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+tPanel.setHoldCount(4000)
+let rPanel = new AutoPlot(130, 250, 650, 230, 'rates',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+rPanel.setHoldCount(4000)
+let lPanel = new AutoPlot(130, 490, 650, 230, 'loads',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+lPanel.setHoldCount(4000)
+
+// ok then... a gather-data routine
+let gather = async () => {
+  try {
+    let results = await Promise.all([tempVM.getExtruderTemp(), loadVM.getReading(false, true), fsVM.getReadings()])
+    let time = TIME.getTimeStamp()
+    // console.log(results[0])
+    tPanel.pushPt([time, results[0]])
+    tPanel.redraw()
+    lPanel.pushPt([time, results[1][0]])
+    lPanel.redraw()
+    // rate is more complex; we have a wheel of some diameter, an 2^14 bits / revolution, so 
+    let diameter = effectiveDriveGearDiameter // * 0.95
+    // the above ~ should be true, since the same drive gear is on the encoder & the motor 
+    // we take rates, etc, from the filament sensor in rads/sec, 
+    let circ = diameter * Math.PI
+    let radsToLinear = circ / (2 * Math.PI)
+    let rate = results[2].rate * radsToLinear
+    rPanel.pushPt([time, rate])
+    rPanel.redraw()
+    return {
+      temp: results[0],
+      load: results[1][0],
+      rate: rate //results[2].rate
+    }
+  } catch (err) {
+    throw err
+  }
+}
+
+let runLoop = false
+let plotLoop = async () => {
+  try {
+    while (runLoop) {
+      await gather()
+      await TIME.awaitFutureTime(TIME.getTimeStamp() + 100)
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+let getDataPoint = async (temp, rate, sampleTime) => {
+  try {
+    console.warn(`GDP: set motor request, and purging`)
+    await extruderMVM.gotoVelocity([rate, 0, 0, 0])
+    // do a (very small pieces of) little purging, 
+    await TIME.delay(500)
+    runLoop = false
+    await TIME.delay(100)
+    console.warn(`GDP: collecting points at ${rate} mm/sec ...`)
+    let stash = []
+    let startTime = TIME.getTimeStamp()
+    let lastGather = TIME.getTimeStamp()
+    let slipCount = 0
+    let broken = false
+    while (TIME.getTimeStamp() - sampleTime < startTime) {
+      // get the new point 
+      let stat = await gather()
+      stash.push(stat)
+      let slip = stat.rate / rate
+      if (slip < 0.75) {
+        slipCount++
+        console.warn(`slip ${slip.toFixed(2)}`)
+      } else {
+        console.log(`slip ${slip.toFixed(2)}`)
+      }
+      if (slipCount > 2) {
+        console.error(`break cond`)
+        broken = true
+        break;
+      }
+      // so, we could / should do some breaking above, likely ? 
+      // hold-up until 100ms since previous collection, 
+      await TIME.awaitFutureTime(lastGather + 100)
+      lastGather = TIME.getTimeStamp()
+    }
+    // calcs: 
+    await extruderMVM.gotoVelocity([0, 0, 0, 0])
+    // next... would work out averages & resolve them in a return, 
+    let avgTemp = 0
+    let avgLoad = 0
+    let avgRate = 0
+    for (let i = 0; i < stash.length; i++) {
+      avgTemp += stash[i].temp
+      avgLoad += stash[i].load
+      avgRate += stash[i].rate
+    }
+    avgTemp /= stash.length
+    avgLoad /= stash.length
+    avgRate /= stash.length
+    console.warn(`GDP: collected ${stash.length} data pts at ${temp}, ${rate}; ${((avgRate / rate) * 100).toFixed(2)} %`)
+    return {
+      temp: avgTemp,
+      load: avgLoad,
+      rate: avgRate,
+      broken: broken,
+      requestedRate: rate,
+      requestedTemp: temp,
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+let run = async () => {
+  try {
+    while (1) {
+      let res = await gather()
+      await TIME.delay(100)
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+// -------------------------------------------------------- Set Extruder Rates...
+
+let extRate = 1
+
+// 29.89 -> 119.94 (100mm requested, 90mm)
+// 29.89 -> -63.24 (100mm requested, 93mm)
+// 0.9 -> 102
+
+let ratePosBtn = new EZButton(10, 120, 100, 100, `set +${extRate}mm/s`)
+let rateZeroBtn = new EZButton(10, 230, 100, 100, `set 0 mm/s`)
+let rateNegBtn = new EZButton(10, 340, 100, 100, `set -${extRate}mm/s`)
+
+ratePosBtn.onClick(() => {
+  extruderMVM.gotoVelocity([extRate, 0, 0, 0]).then(() => {
+    ratePosBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    ratePosBtn.bad()
+  })
+})
+
+rateZeroBtn.onClick(() => {
+  extruderMVM.gotoVelocity([0, 0, 0, 0]).then(() => {
+    rateZeroBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    rateZeroBtn.bad()
+  })
+})
+
+rateNegBtn.onClick(() => {
+  extruderMVM.gotoVelocity([-extRate, 0, 0, 0]).then(() => {
+    rateNegBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    rateNegBtn.bad()
+  })
+})
+
+let hotTemp = 240
+let setHotBtn = new EZButton(10, 450, 100, 100, `set ${hotTemp}`)
+let setColdBtn = new EZButton(10, 560, 100, 100, `set 0`)
+
+setHotBtn.onClick(() => {
+  tempVM.setExtruderTemp(hotTemp).then(() => {
+    setHotBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    setHotBtn.bad()
+  })
+})
+
+setColdBtn.onClick(() => {
+  tempVM.setExtruderTemp(0).then(() => {
+    setColdBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    setColdBtn.bad()
+  })
+})
+
+/*
+
+// -------------------------------------------------------- Filament Sensor 
+
+let fsTestBtn = new Button(120, 10, 84, 184, 'fs test')
+let fsTest = () => {
+  fsVM.getBundle().then((data) => {
+    fsTestBtn.setHTML(`
+    dia: ${data.diameter.toFixed(3)}<br>pos: ${data.posEstimate.toFixed(2)}<br>rate: ${data.rateEstimate.toFixed(2)}
+    `)
+    //console.log(data)
+    setTimeout(fsTest, 50)
+  }).catch((err) => {
+    console.error(err)
+  })
+}
+fsTestBtn.onClick(fsTest)
+
+
+// // collect a # of samples, 
+// let lastTemp = 0
+// let dataStore = []
+// let gather = async (samples, temp = 500) => {
+//   let collect = () => {
+//     return new Promise((resolve, reject) => {
+//       // we can gather all of the data w/ this promise.all thing: 
+//       Promise.all(
+//         [tempVM.getExtruderTemp(), loadVM.getReading()] // fsVM.getStates(), loadVM.getReading(),]
+//       ).then((values) => {
+//         //console.log('all', values)
+//         let time = TIMES.getTimeStamp()
+//         tPanel.pushPt([time, values[0]])
+//         rPanel.pushPt([time, values[1].rateEstimate])
+//         lPanel.pushPt([time, values[2][0]])
+//         lastTemp = values[0]
+//         dataStore.push([values[0], values[1].rateEstimate, values[2][0]])
+//         tPanel.redraw()
+//         rPanel.redraw()
+//         lPanel.redraw()
+//         resolve()
+//       }).catch((err) => {
+//         console.error(err)
+//       })
+//     })
+//   }
+//   while (samples > 0) {
+//     try {
+//       await collect()
+//       await TIMES.delay(100)
+//       if (lastTemp > temp) break;
+//       //console.log(samples)
+//       samples--
+//     } catch (err) {
+//       console.error(err)
+//     }
+//   }
+// }
+
+// -------------------------------------------------------- Experiment 
+// this one can be nasteh simple, 
+
+let fsPollBtn = new Button(120, 210, 84, 184, 'run...')
+let pollPt = async (temp, rate, feed) => {
+  // assuming everything is reset here... 
+  try {
+    console.warn(`generating pt for ${temp}, ${rate}, ${feed}`)
+    // reconfig motor, 
+    //console.warn(`reconfig motor rate...`)
+    extruderMVM.motion.settings = {
+      junctionDeviation: 0.05,
+      accelLimits: [200],
+      velLimits: [rate]
+    }
+    await extruderMVM.setup()
+    // await temp, 
+    //console.warn(`awaiting ${temp}...`)
+    await tempVM.awaitExtruderTemp(temp)
+    // request + 5mm, to purge...
+    //console.warn(`pushing 15mm purge`)
+    await extruderMVM.motion.delta([5])
+    await TIMES.delay((5 / rate) * 1000 + 1000)
+    // get another zero, 
+    //console.warn(`gathering zero...`)
+    let data = await fsVM.getBundle()
+    fsTestBtn.setHTML(`
+    dia: ${data.diameter.toFixed(3)}<br>pos: ${data.posEstimate.toFixed(2)}<br>rate: ${data.rateEstimate.toFixed(2)}
+    `)
+    let zeroWheelPos = data.posEstimate
+    // extrude... 
+    //console.warn(`extruding ${feed} mm`)
+    await extruderMVM.motion.delta([feed])
+    await TIMES.delay((feed / rate) * 1000 + 1000)
+    data = await fsVM.getBundle()
+    console.warn(`requested ${feed} at ${temp} with ${rate} mm/s rolled ${(data.posEstimate - zeroWheelPos).toFixed(2)}`)
+    //console.warn('DONE!')
+    return data.posEstimate - zeroWheelPos
+  } catch (err) {
+    throw err
+  }
+}
+// should loop thru temps, rates... 
+// aye
+// i 
+// would like to add a power-cycle between temp passes, as there is some mixed up motor 
+// code (or something) that is messing w/ the integrator, causing it to step backwards 
+// also I could write code that intelligently increases rates up to some % rolloff, like 20% say, 
+// then we actually would see expanding temp... up to some max of whatever the hotend can do 
+let results = []
+let shutdown = false 
+let sweepPts = async (exP) => {
+  try {
+    for (let temp = exP.tempStart; temp <= exP.tempEnd; temp += exP.tempIncrement) {
+      // power cycle...
+      await powerVM.setPowerStates(false, false)
+      await TIMES.delay(50)
+      await powerVM.setPowerStates(true, true)
+      await TIMES.delay(500)
+      for (let rate = exP.rateStart; rate <= exP.rateEnd; rate += exP.rateIncrement) {
+        let polls = []
+        for (let c = 0; c < exP.count; c++) {
+          if(shutdown) throw new Error('bailing for shutdown')
+          polls.push(await pollPt(temp, rate, exP.feed))
+        }
+        let result = {
+          temp: temp,
+          rate: rate,
+          feedRequested: exP.feed,
+          feedActual: polls // await pollPt(temp, rate, exP.feed),
+        }
+        results.push(result)
+        // calculate % actual feed,
+        let actual = polls.reduce((a, b) => { return a + b }, 0) / polls.length 
+        let percent = actual / exP.feed 
+        console.warn(`MADE ${percent.toFixed(2)}...`)
+        if(percent < 0.7){
+          console.warn(`BAILING w/ ${percent.toFixed(2)} extrusion`)
+          break; // should break rate-loop, right ? 
+        }
+      }
+    }// end for-temps, 
+    console.log(results)
+  } catch (err) {
+    console.error(err)
+  }
+  // try to get partials... 
+  SaveFile(results, 'json', 'pla-esun')
+  // for(let temp = start; temp < end; temp += increment){
+  //   try {
+  //     for(let i = 0; i < count; i ++){
+  //       await pollPt(temp, 2, 25)
+  //     }
+  //   } catch (err) {
+  //     console.error(err)
+  //   }
+  // }
+  console.warn(`DONE SWEEP!`)
+}
+fsPollBtn.onClick(() => {
+  sweepPts({
+    tempStart: 190,
+    tempEnd: 290, // beyond 300 degs we have maybe a calibration issue w/ the t-couple 
+    tempIncrement: 15,
+    rateStart: 2,
+    rateEnd: 22,
+    rateIncrement: 4,
+    count: 3,
+    feed: 25,
+  })
+})
+
+let shutdownBtn = new Button(120, 420, 84, 84, 'shdn')
+shutdownBtn.onClick(async () => {
+  shutdown = true 
+  await powerVM.setPowerStates(true, true)
+})
+
+// alright next... what would be next ? motion controller ? pressures ? bed level ?
+// where in the gantt are we ? 
+
+// -------------------------------------------------------- Config Extruder Motor 
+
+let effectiveDriveGearDiameter = 8.2 * 0.9
+
+extruderMVM.motion.settings = {
+  junctionDeviation: 0.05,
+  accelLimits: [100],
+  velLimits: [10]
+}
+
+extruderMVM.settings.motor = {
+  axis: 0,
+  invert: false,
+  microstep: 16,
+  spu: (16 * 200) / (effectiveDriveGearDiameter * Math.PI),
+  cscale: 0.25
+}
+
+// we'll want to track motor setup, 
+
+let configStateBtn = new Button(10, 10, 100, 100, `setup config...`)
+let configSys = async () => {
+  try {
+    await powerVM.setPowerStates(true, false)
+    await extruderMVM.setup()
+    configStateBtn.green('sys ok')
+  } catch (err) {
+    console.error(err)
+    configStateBtn.red('bad setup...')
+  }
+}
+setTimeout(configSys, 500)
+
+
+
+// -------------------------------------------------------- Panels 
+
+// spare temp controller to load / unload: 
+let tempPanel = new TempPanel(tempVM, 220, 10, 200, 'hotend')
+
+*/
+
+/*
+let tPanel = new AutoPlot(220, 10, 650, 230, 'hotend',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+tPanel.setHoldCount(4000)
+let rPanel = new AutoPlot(220, 250, 650, 230, 'rates',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+rPanel.setHoldCount(4000)
+let lPanel = new AutoPlot(220, 490, 650, 230, 'loads',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+lPanel.setHoldCount(4000)
+
+// ok then... a gather-data routine
+
+// collect a # of samples, 
+let lastTemp = 0
+let dataStore = []
+let gather = async (samples, temp = 500) => {
+  let collect = () => {
+    return new Promise((resolve, reject) => {
+      // we can gather all of the data w/ this promise.all thing: 
+      Promise.all(
+        [tempVM.getExtruderTemp(), quantickVM.getStates(), loadVM.getReading(),]
+      ).then((values) => {
+        //console.log('all', values)
+        let time = TIMES.getTimeStamp()
+        tPanel.pushPt([time, values[0]])
+        rPanel.pushPt([time, values[1].rateEstimate])
+        lPanel.pushPt([time, values[2][0]])
+        lastTemp = values[0]
+        dataStore.push([values[0], values[1].rateEstimate, values[2][0]])
+        tPanel.redraw()
+        rPanel.redraw()
+        lPanel.redraw()
+        resolve()
+      }).catch((err) => {
+        console.error(err)
+      })
+    })
+  }
+  while (samples > 0) {
+    try {
+      await collect()
+      await TIMES.delay(100)
+      if (lastTemp > temp) break;
+      //console.log(samples)
+      samples--
+    } catch (err) {
+      console.error(err)
+    }
+  }
+}
+
+let runExperiment = async () => {
+  try {
+    console.warn('setup')
+    await tempVM.setPCF(0)
+    await gather(10)
+    console.warn('setting torque')
+    await quantickVM.setTorque(-0.3)
+    console.warn('gathering')
+    await gather(50)
+    for (let temp = 170; temp <= 240; temp += 10) {
+      console.warn(`setting ${temp}...`)
+      await tempVM.setExtruderTemp(temp)
+      await gather(2000, temp)
+      console.warn(`gathering for ${temp}`)
+      await gather(400)
+    }
+    console.warn('done')
+    SaveFile(dataStore, 'json', 'extruderData')
+    await tempVM.setExtruderTemp(0)
+    await gather(200)
+  } catch (err) {
+    console.error(err)
+  }
+}
+*/
+
+// this starts the experiment ! 
+// setTimeout(runExperiment, 500)
+
+// -------------------------------------------------------- Power States 
+// also, keepalive & connection indicator 
+/*
+let kaIndicator = new Button(10, 10, 84, 84, 'connection ?')
+kaIndicator.yellow()
+let v5State = false
+let v24State = false
+let v5Btn = new Button(10, 110, 84, 44, '5V Power')
+let v24Btn = new Button(10, 170, 84, 44, '24V Power')
+v24Btn.yellow()
+v5Btn.yellow()
+let powerTimerLength = 1000
+let powerTimer = {}
+
+// we want to run these on a loop... 
+let checkPowerStates = () => {
+  motionVM.getPowerStates().then((data) => {
+    kaIndicator.green('Connection OK')
+    if (data[0]) {
+      v5Btn.green('5V Power')
+      v5State = true
+    } else {
+      v5Btn.grey('5V Power')
+      v5State = false
+    }
+    if (data[1]) {
+      v24Btn.green('24V Power')
+      v24State = true
+    } else {
+      v24Btn.grey('24V Power')
+      v24State = false
+    }
+    clearTimeout(powerTimer)
+    powerTimer = setTimeout(checkPowerStates, powerTimerLength)
+  }).catch((err) => {
+    console.error(err)
+    kaIndicator.red('keepalive broken, see console')
+    v5Btn.red('see console')
+    v24Btn.red('see console')
+  })
+}
+
+v5Btn.onClick(async () => {
+  v5Btn.yellow()
+  await motionVM.setPowerStates(!v5State, v24State)
+  checkPowerStates()
+})
+
+v24Btn.onClick(async () => {
+  v24Btn.yellow()
+  await motionVM.setPowerStates(v5State, !v24State)
+  checkPowerStates()
+})
+
+// startup w/ this loop; 
+// comment this line out if you don't want the keepalive to bother you while you try 
+// hardware-less code stuff 
+powerTimer = setTimeout(checkPowerStates, 750)
+*/
+
+// -------------------------------------------------------- Initializing the WSC Port 
+
+// verbosity 
+let LOGPHY = false
+// to test these systems, the client (us) will kickstart a new process
+// on the server, and try to establish connection to it.
+console.log("making client-to-server request to start remote process,")
+console.log("and connecting to it w/ new websocket")
+
+let wscVPortStatus = "opening"
+// here we attach the "clear to send" function,
+// in this case we aren't going to flowcontrol anything, js buffers are infinite
+// and also impossible to inspect  
+wscVPort.cts = () => { return (wscVPortStatus == "open") }
+// we also have isOpen, similarely simple here, 
+wscVPort.isOpen = () => { return (wscVPortStatus == "open") }
+
+// ok, let's ask to kick a process on the server,
+// in response, we'll get it's IP and Port,
+// then we can start a websocket client to connect there,
+// automated remote-proc. w/ vPort & wss medium,
+// for args, do '/processName.js?args=arg1,arg2'
+jQuery.get('/startLocal/osapSerialBridge.js', (res) => {
+  if (res.includes('OSAP-wss-addr:')) {
+    let addr = res.substring(res.indexOf(':') + 2)
+    if (addr.includes('ws://')) {
+      wscVPortStatus = "opening"
+      // start up, 
+      console.log('starting socket to remote at', addr)
+      let ws = new WebSocket(addr)
+      ws.binaryType = "arraybuffer"
+      // opens, 
+      ws.onopen = (evt) => {
+        wscVPortStatus = "open"
+        // implement rx
+        ws.onmessage = (msg) => {
+          let uint = new Uint8Array(msg.data)
+          wscVPort.receive(uint)
+        }
+        // implement tx 
+        wscVPort.send = (buffer) => {
+          if (LOGPHY) console.log('PHY WSC Send', buffer)
+          ws.send(buffer)
+        }
+      }
+      ws.onerror = (err) => {
+        wscVPortStatus = "closed"
+        console.log('sckt err', err)
+      }
+      ws.onclose = (evt) => {
+        wscVPortStatus = "closed"
+        console.log('sckt closed', evt)
+      }
+    }
+  } else {
+    console.error('remote OSAP not established', res)
+  }
+})
\ No newline at end of file
diff --git a/system/javascript/client/index.html b/system/javascript/client/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..1474c6a257e7bf79f7028178f1e7cd18dfd5e924
--- /dev/null
+++ b/system/javascript/client/index.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>clank-tool</title>
+    <!-- these three disable caching -->
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="-1" />
+</head>
+
+<body>
+    <link href="style.css" rel="stylesheet">
+    <script src="../osapjs/client/libs/jquery.min.js"></script>
+    <script src="../osapjs/client/libs/math.js" type="text/javascript"></script>
+    <script src="../osapjs/client/libs/d3.js"></script>
+    <script type="module" src="flowMaps.js"></script>
+
+    <div id="wrapper">
+    	<!-- bootloop puts first view inside of this div -->
+    </div>
+
+</body>
+
+</html>
diff --git a/system/javascript/client/slumpMaps.js b/system/javascript/client/slumpMaps.js
new file mode 100644
index 0000000000000000000000000000000000000000..f252b680889046237d6054b5344d055270527cab
--- /dev/null
+++ b/system/javascript/client/slumpMaps.js
@@ -0,0 +1,718 @@
+/*
+slumpMaps.js
+
+builds temp-slump maps for lpf project 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+// core elements 
+import OSAP from '../osapjs/core/osap.js'
+import TIME from '../osapjs/core/time.js'
+import PK from '../osapjs/core/packets.js'
+
+// virtual machines 
+import PowerSwitchVM from '../osapjs/vms/powerSwitches.js'
+import AXLActuator from '../osapjs/vms/axlActuator.js'
+import AXLCore from '../osapjs/vms/axlCore.js'
+import TempVM from '../osapjs/vms/tempVirtualMachine.js'
+import LoadVM from '../osapjs/vms/loadcellVirtualMachine.js'
+import FilamentSensorVM from '../osapjs/vms/filamentSensorVirtualMachine.js'
+
+import snakeGen from '../osapjs/test/snakeGen.js'
+
+import { SaveFile } from '../osapjs/client/utes/saveFile.js'
+
+// ui elements 
+import Grid from '../osapjs/client/interface/grid.js' // main drawing API 
+import { Button, EZButton, TextBlock, TextInput } from '../osapjs/client/interface/basics.js'
+import TempPanel from '../osapjs/client/components/tempPanel.js'
+import AutoPlot from '../osapjs/client/components/autoPlot.js'
+
+console.log(`------------------------------------------`)
+console.log("hello lpf controller")
+
+// -------------------------------------------------------- The main UI... thing 
+
+let grid = new Grid()
+
+// -------------------------------------------------------- OSAP Object
+let osap = new OSAP("slump-tester")
+
+// -------------------------------------------------------- SETUP NETWORK / PORT 
+let wscVPort = osap.vPort("wscVPort")
+
+
+// -------------------------------------------------------- Yonder Clank VM and Motors 
+/*
+//OSAP osap("axl-stepper_z-rear-left");
+//OSAP osap("axl-stepper_z-front-left");
+//OSAP osap("axl-stepper_z-rear-right");
+//OSAP osap("axl-stepper_z-front-right");
+//OSAP osap("axl-stepper_y-left");
+//OSAP osap("axl-stepper_y-right");
+//OSAP osap("axl-stepper_x");
+OSAP osap("axl-stepper_e");
+*/
+
+let xyMicroStep = 8
+let xySPU = 40
+let xyCScale = 0.3
+
+let zMicroStep = 4
+let zSPU = 53.57142857 * 2
+let zCScale = 0.3
+
+// we should ~ fudge EDGD if anything, 
+let effectiveDriveGearDiameter = 8.2 * 1.0
+let eMicroStep = 8
+// microstep * 200 = total steps / revolution 
+// gearDia * PI is circumference, for steps / linear mm, 
+// then /2.41, for linear mm -> cubic mm (one mm of 1.75mm dia filament is 2.41 mm^3 of filament)
+let eSPU = ((eMicroStep * 200) / (effectiveDriveGearDiameter * Math.PI)) / 2.41
+let eCScale = 0.25
+
+let clank = new AXLCore(osap, {
+  bounds: [254, 122, 199.4, 100],
+  accelLimits: [1500, 1500, 100, 100],
+  velocityLimits: [150, 150, 40, 25],
+  queueStartDelay: 500,
+  junctionDeviation: 0.1
+}, [
+  {
+    name: "rt_axl-stepper_x",
+    axis: 0,
+    invert: false,
+    microstep: xyMicroStep,
+    spu: xySPU,
+    cscale: xyCScale
+  },
+  {
+    name: "rt_axl-stepper_y-left",
+    axis: 1,
+    invert: true,
+    microstep: xyMicroStep,
+    spu: xySPU,
+    cscale: xyCScale
+  },
+  {
+    name: "rt_axl-stepper_y-right",
+    axis: 1,
+    invert: false,
+    microstep: xyMicroStep,
+    spu: xySPU,
+    cscale: xyCScale
+  },
+  {
+    name: "rt_axl-stepper_z-rear-left",
+    axis: 2,
+    invert: false,
+    microstep: zMicroStep,
+    spu: zSPU,
+    cscale: zCScale
+  },
+  {
+    name: "rt_axl-stepper_z-rear-right",
+    axis: 2,
+    invert: true,
+    microstep: zMicroStep,
+    spu: zSPU,
+    cscale: zCScale
+  },
+  {
+    name: "rt_axl-stepper_z-front-left",
+    axis: 2,
+    invert: true,
+    microstep: zMicroStep,
+    spu: zSPU,
+    cscale: zCScale
+  },
+  {
+    name: "rt_axl-stepper_z-front-right",
+    axis: 2,
+    invert: false,
+    microstep: zMicroStep,
+    spu: zSPU,
+    cscale: zCScale
+  },
+  {
+    name: "rt_axl-stepper_e",
+    axis: 3,
+    invert: false,
+    microstep: eMicroStep,
+    spu: eSPU,
+    cscale: eCScale
+  },
+])
+
+// this clank has 1:1 transforms, this is a hack to overwrite 
+// the default (shouldn't be) axl corexy tfs
+clank.cartesianToActuatorTransform = (vals, position = false) => {
+  let tfVals = JSON.parse(JSON.stringify(vals))
+  return tfVals
+}
+
+clank.actuatorToCartesianTransform = (vals, position = false) => {
+  let tfVals = JSON.parse(JSON.stringify(vals))
+  return tfVals
+}
+
+// -------------------------------------------------------- Ute to switch system power at the motion-head circuit
+
+// this one is built "the new way" 
+// the psu-head / modular-motion-head board: which is just a bus router and power switches to us, 
+let powerSwitchVM = new PowerSwitchVM(osap)
+
+// these are older code, we want global vars for but can't constructor them until we have a route:
+// the extruder motor 
+let extruderMVM = {}
+// temperature / heater module(s)
+let tempVM = {}
+let bedTempVM = {}
+// filament sensor 
+let fsVM = {}
+// load sensor, 
+let loadVM = {}
+
+// -------------------------------------------------------- Setup Code 
+
+let setup = async () => {
+  try {
+    setupBtn.yellow('switches...')
+    console.warn(`SETUP: finding switches...`)
+    await powerSwitchVM.setup()
+    setupBtn.yellow('power cycle...')
+    console.warn(`SETUP: cycling power...`)
+    await powerSwitchVM.setPowerStates(false, false)
+    await TIME.delay(500)
+    await powerSwitchVM.setPowerStates(true, false)
+    await TIME.delay(250)
+    await powerSwitchVM.setPowerStates(true, true)
+    await TIME.delay(1500)
+    // start a-lookin, 
+    setupBtn.yellow('graph traverse...')
+    console.warn(`SETUP: collecting graph...`)
+    let graph = await osap.nr.sweep()
+    // console.log(graph)
+    // return 
+    // can we get the peripherals ? 
+    // collect and setup tempvm, 
+    setupBtn.yellow('heater...')
+    console.warn(`SETUP: collecting heater module...`)
+    tempVM = new TempVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_heater-module", graph)).route))
+    await tempVM.setExtruderTemp(200)
+    setupBtn.yellow('loadcell...')
+    console.warn(`SETUP: collecting loadcells...`)
+    loadVM = new LoadVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_loadcell-amp", graph)).route))
+    runLoop = true
+    plotLoop()
+    // console.warn(`SETUP: collecting bed heater...`)
+    // bedTempVM = new TempVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_bed-heater-module", graph)).route))
+    // console.warn(`SETUP: collecting filament sensor...`)
+    // fsVM = new FilamentSensorVM(osap, PK.VC2VMRoute((await osap.nr.find("rt_filament-sensor", graph)).route))
+    // collect and setup our clank;
+    setupBtn.yellow('machine...')
+    console.warn(`SETUP: hooking machine...`)
+    await clank.setup(graph)
+    console.warn(`SETUP: clank appears to be config'd, etc`)
+    setupBtn.yellow('homing...')
+    await clank.home(graph)
+    console.warn(`SETUP: clank is homed...`)
+    setupBtn.green('done!')
+    console.log(`------------------------------------------`)
+    return
+    // comment / uncomment these to run the experiment or just plot (to watch temps drop for safe power down)
+    // runLoop = true 
+    // plotLoop()
+    // return 
+    // SaveFile(tempSamples, 'json', 'abs-natural-matterhackers')
+    // await run()
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+setTimeout(setup, 750)
+
+// -------------------------------------------------------- Running... 
+
+let runBtn = new Button({
+  xPlace: 10,
+  yPlace: 670,
+  width: 100,
+  height: 100,
+  defaultText: `run test...`
+})
+
+runBtn.onClick(() => {
+  // non-slumpy:  200, 5, 20, 1.0
+  // slumpy:      260, 22, 10, 0.0
+  slumpTest(270, 25, 15, 0.0)
+  // I'm sort of sick of this, so I want to 
+  // show for one-hundo that we can get some signal, 
+  // let's do 260, 22, 10 w/ no PCF, 
+  // then the same w/ maxed out PCF... 
+})
+
+let slumpTest = async (temp, rate, sideLength, pcf) => {
+  // rate should be expressed in cubic mm / sec, right ? then we transform to 
+  // linear etc 
+  try {
+    // we should do a purge, non ? 
+    // we'll use a long snakegen thing,
+    let purge = snakeGen({
+      width: 120,
+      depth: 10,
+      height: 0.4,
+      trackWidth: 0.4,
+      trackHeight: 0.4,
+      segmentLength: 120,
+      flowRate: rate 
+    })
+    console.log(`purge = `, purge)
+    for(let seg of purge){
+      seg.target[0] += 10 
+      seg.target[1] += 20 
+    }
+    let absoluteEValueHack = purge[purge.length - 1].target[3]
+    // set the temp, and turn the cooling fan off 
+    await tempVM.setPCF(pcf)
+    await tempVM.awaitExtruderTemp(temp)
+    // this'll be useful, 
+    // let sideLength = Math.sqrt(area)
+    // we'll rebuild this as a full-fledged path now, doing some flow-rate maths 
+    // to set our linear rate... 
+    purge[0].rate = 10 // move *down* at this rate, not too fast... 
+    for (let seg of purge) {
+      await clank.addMoveToQueue(seg)
+    }
+    await clank.awaitMotionEnd()
+    // ok then, should be purge-tino, let's do the real deal... 
+    // let's do one test layer over here to the right... 
+    // and these should be big enough to get "real signal" from, 
+    let oneLayer = snakeGen({
+      width: sideLength,
+      depth: sideLength,
+      height: 0.4,
+      trackWidth: 0.4,
+      trackHeight: 0.4,
+      segmentLength: sideLength,
+      flowRate: rate
+    })
+    // now run it on the floor, and in the air:
+    let floorRun = JSON.parse(JSON.stringify(oneLayer))
+    let airRun = JSON.parse(JSON.stringify(oneLayer))
+    for (let s in floorRun) {
+      // shift floor run over to the right, 
+      floorRun[s].target[0] += sideLength + 10
+      floorRun[s].target[1] += 40
+      floorRun[s].target[3] += absoluteEValueHack
+    }
+    absoluteEValueHack = floorRun[floorRun.length - 1].target[3]
+    for(let s in airRun){
+      // we basically need / should-do relative E moves, at least into the core
+      // i'll do that next... 
+      // then we should be OK to try this, which is getting better and better otherwise... 
+      // shift air run the same, and... into the air, 
+      airRun[s].target[0] += sideLength + 10
+      airRun[s].target[1] += 20
+      airRun[s].target[2] += 40
+      airRun[s].target[3] += absoluteEValueHack
+    }
+    airRun[0].rate = 10
+    absoluteEValueHack = airRun[airRun.length - 1].target[3]
+    // and... yep, run 'em, 
+    for (let seg of airRun) {
+      await clank.addMoveToQueue(seg)
+    }
+    await clank.awaitMotionEnd()
+    await TIME.delay(2500)
+    // for (let seg of airRun) {
+    //   await clank.addMoveToQueue(seg)
+    // }
+    // await clank.awaitMotionEnd()
+    // // hang for a little 
+    // await TIME.delay(5000)
+    // and run the real dealio;
+    let slumpPath = snakeGen({
+      width: sideLength,
+      depth: sideLength,
+      height: 20,
+      trackWidth: 0.4,
+      trackHeight: 0.4,
+      segmentLength: sideLength,
+      flowRate: rate
+    })
+    // and run it... 
+    slumpPath[0].rate = 10 
+    for (let seg of slumpPath) {
+      // if(seg.target[2] == 0.4) seg.rate = seg.rate * 0.2 // do a slow 1st layer 
+      seg.target[0] += 80
+      seg.target[1] += 80
+      seg.target[3] += absoluteEValueHack
+      console.log(`e ${seg.target[3].toFixed(2)}`)
+      await clank.addMoveToQueue(seg)
+    }
+    await clank.awaitMotionEnd()
+    await clank.moveRelative([0, 0, 80, 0])
+    SaveFile(dataStash, 'json', 'hotStash')
+  } catch (err) {
+    console.error(err)
+    runBtn.red('err!')
+    try {
+      tempVM.setExtruderTemp(0)
+    } catch (err) {
+      console.error(`failed to cooldown extruder temp, also (!)`)
+      console.error(err)
+    }
+  }
+}
+
+// ok we do need... the purge, then the zero-spec pressure, and the empty-space pressure... 
+// then if we can show hi-pressure, lo-pressure, 
+// and a dropoff during a print, we have slumping signal 
+/*
+
+this is tough... I think maybe I should purge, then air print, then do the 1st layer and 
+onwards, rather than trying to go down, up, down... which maybe is about to be fixed though 
+... but maybe i.e. non-slumping prints eventually recover *up* from the air print
+whereas slumping prints always stay (even at the ground) near the air-print temps ?? 
+it's also worth considering... just lowering the layer height, for more pressure 
+and ~ using a 0.6mm nozzle for more "fidelity" / less overall sensitivity (?) 
+
+and maybe some other measure, like pressure-per-flowrate, is better than these 
+absolute values, when the machine is accelerating all over the place ? 
+
+shit man, alright, I'm trying with:
+(1) the purge 
+(2) the air run 
+(3) 0->N layers 
+
+Thesis being that... 
+- slumpy prints will either quickly drop, or stay at air-run pressure 
+- non-slumpy prints will either recover from a shit first layer, or ~ won't drop to these levels ?
+- i.e. slumpy: avg. down slope
+- non-slumpy: avg. flat or lifting slope 
+
+I think I'm going to quit here either way, it's 320 and I need enough time to look at the "analysis"
+... though might do one more w/ the same size here, but hot and fast and w/o PCF... it's more legible 
+
+on return to the machine,
+- we want to be able to do "per layer average nozzle pressure" and plot those, basically 
+  - so, get "air layer nozzle pressure" then subsequent...
+  - should allow us to calculate... like, asymtotic approach... slope and final resting place 
+- and like, we would want to measure layer times, as well, during the test... use a little 
+  more theory when developing this test in general: what do you expect will be the pattern? 
+
+PAPER NOTES
+
+want to show that we can "observe" changes to the machine model, or ~
+observe that changes to the machine, change the machine:material model, 
+so we should have two sets of slumping data, one with the fan on full tilt
+and the other w/o the fan... 
+*/
+
+/*
+NOTES
+
+for the next (closed loop realtime) steps, we should totally get in touch with jacob white 
+about what level of systems ID *he* would start with, to develop controllers 
+for this thing automagically... 
+
+takeaways from this slumping session... 
+- PCF does a lot, hot dang 
+  - new hotend should get big fukken blowers then, pwm'able, yeah ? 
+- pressure... is even when extrusion is even, and can see ~ that over-extrusion hairiness 
+
+it'd be tite to figure out why the response to these limit switch inputs is so 
+gd long ? OSAP-cores should measure avg loop cycle times... 
+
+and... the new hotend design: it'd be awesome to be able to have good view to the nozzle 
+
+also thinking about the bus: we could do some two-step drop-and-head non-contentious scheduling,
+so the head always arbitrates, taps, and does timing, but i.e. drops announce they have 
+packets, then the head dishes time accordingly ?? 
+
+it looks like, for the hotend CAD, the part cooling fan direction also counts a great deal: 
+we want all sides (or at least two, symmetric) to get hit evenly 
+*/
+
+// -------------------------------------------------------- UI Elements 
+
+// -------------------------------------------------------- Temp Panel / Etc 
+
+let tPanel = new AutoPlot(130, 10, 650, 230, 'hotend',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+tPanel.setHoldCount(4000)
+// let bPanel = new AutoPlot(130, 250, 650, 230, 'bed',
+//   { top: 40, right: 20, bottom: 30, left: 60 })
+// bPanel.setHoldCount(2000)
+let lPanel = new AutoPlot(130, 250, 650, 230, 'loads',
+  { top: 40, right: 20, bottom: 30, left: 60 })
+lPanel.setHoldCount(4000)
+
+let dataStash = []
+
+// ok then... a gather-data routine
+let gather = async () => {
+  try {
+    // let results = await Promise.all([tempVM.getExtruderTemp(), loadVM.getReading(false, true), fsVM.getReadings(), bedTempVM.getExtruderTemp()])
+    let time = TIME.getTimeStamp()
+    let results = await Promise.all([tempVM.getExtruderTemp(), loadVM.getReading(false, true)])
+    // console.log(results[0])
+    tPanel.pushPt([time, results[0]])
+    tPanel.redraw()
+    lPanel.pushPt([time, results[1][0]])
+    lPanel.redraw()
+    dataStash.push({ temp: results[0], load: results[1][0] })
+    // bPanel.pushPt([time, await bedTempVM.getExtruderTemp()])
+    // bPanel.redraw()
+    /*
+    // rate is more complex; we have a wheel of some diameter, an 2^14 bits / revolution, so 
+    let diameter = 17.5   // TODO this... is a hack (!) real diameter is ~ 20.72, now, but we *need* to redesign 
+      // the filament sensor... so that it's rate == the motor's rate, no guessy guessy 
+    let ticks = 16384
+    let circ = diameter * Math.PI
+    let tickToLinear = circ / ticks
+    let rate = results[2].rate * tickToLinear
+    rPanel.pushPt([time, rate])
+    rPanel.redraw()
+    */
+    return
+    return {
+      temp: results[0],
+      bedTemp: results[1],
+    }
+  } catch (err) {
+    throw err
+  }
+}
+
+let runLoop = false
+let plotLoop = async () => {
+  try {
+    while (runLoop) {
+      await gather()
+      await TIME.awaitFutureTime(TIME.getTimeStamp() + 100)
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+let getDataPoint = async (temp, rate, sampleTime) => {
+  try {
+    // first we would want to set a temp and wait for it,
+    console.warn(`GDP: set / await temp...`)
+    runLoop = true
+    plotLoop()
+    await tempVM.setExtruderTemp(temp)
+    // let's await like this... 
+    await tempVM.awaitExtruderTemp(temp)
+    console.warn(`GDP: set motor request, and purging`)
+    await extruderMVM.gotoVelocity([rate, 0, 0])
+    // do a little purging, 
+    await TIME.delay(5000)
+    runLoop = false
+    await TIME.delay(100)
+    console.warn(`GDP: collecting points...`)
+    let stash = []
+    let startTime = TIME.getTimeStamp()
+    let lastGather = TIME.getTimeStamp()
+    while (TIME.getTimeStamp() - sampleTime < startTime) {
+      // get new pt, 
+      stash.push(await gather())
+      // hold-up until 100ms since previous collection, 
+      await TIME.awaitFutureTime(lastGather + 100)
+      lastGather = TIME.getTimeStamp()
+    }
+    await extruderMVM.gotoVelocity([0, 0, 0])
+    // next... would work out averages & resolve them in a return, 
+    let avgTemp = 0
+    let avgLoad = 0
+    let avgRate = 0
+    for (let i = 0; i < stash.length; i++) {
+      avgTemp += stash[i].temp
+      avgLoad += stash[i].load
+      avgRate += stash[i].rate
+    }
+    avgTemp /= stash.length
+    avgLoad /= stash.length
+    avgRate /= stash.length
+    console.warn(`GDP: collected ${stash.length} data pts at ${temp}, ${rate}; ${((avgRate / rate) * 100).toFixed(2)} %`)
+    return {
+      temp: avgTemp,
+      load: avgLoad,
+      rate: avgRate,
+      requestedRate: rate,
+      requestedTemp: temp,
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+let run = async () => {
+  try {
+    while (1) {
+      let res = await gather()
+      await TIME.delay(200)
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+// -------------------------------------------------------- Onlining... 
+
+let setupBtn = new Button({
+  xPlace: 10,
+  yPlace: 10,
+  width: 100,
+  height: 100,
+  defaultText: `setup...`
+})
+
+// -------------------------------------------------------- Set Extruder Rates...
+
+let extRate = 5
+
+let ratePosBtn = new EZButton(10, 120, 100, 100, `set +${extRate}mm/s`)
+let rateZeroBtn = new EZButton(10, 230, 100, 100, `set 0 mm/s`)
+let rateNegBtn = new EZButton(10, 340, 100, 100, `set -${extRate}mm/s`)
+
+ratePosBtn.onClick(() => {
+  clank.gotoVelocity([0, 0, 0, extRate]).then(() => {
+    ratePosBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    ratePosBtn.bad()
+  })
+})
+
+rateZeroBtn.onClick(() => {
+  clank.gotoVelocity([0, 0, 0, 0]).then(() => {
+    rateZeroBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    rateZeroBtn.bad()
+  })
+})
+
+rateNegBtn.onClick(() => {
+  clank.gotoVelocity([0, 0, 0, -extRate]).then(() => {
+    rateNegBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    rateNegBtn.bad()
+  })
+})
+
+let hotTemp = 220
+let setHotBtn = new EZButton(10, 450, 100, 100, `set ${hotTemp}`)
+let setColdBtn = new EZButton(10, 560, 100, 100, `set 0`)
+
+setHotBtn.onClick(() => {
+  tempVM.setExtruderTemp(hotTemp).then(() => {
+    setHotBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    setHotBtn.bad()
+  })
+})
+
+setColdBtn.onClick(() => {
+  tempVM.setExtruderTemp(0).then(() => {
+    setColdBtn.good()
+  }).catch((err) => {
+    console.error(err)
+    setColdBtn.bad()
+  })
+})
+
+// -------------------------------------------------------- Motor Disable
+
+let noMotorBtn = new Button({
+  xPlace: 10,
+  yPlace: 780,
+  width: 100,
+  height: 100,
+  defaultText: `disable motors...`
+})
+
+noMotorBtn.onClick(async () => {
+  try {
+    SaveFile(dataStash, 'json', 'hotStash')
+    await clank.disableMotors()
+  } catch (err) {
+    noMotorBtn.red('err disabling!')
+    console.error(err)
+  }
+})
+
+// -------------------------------------------------------- Initializing the WSC Port 
+
+// verbosity 
+let LOGPHY = false
+// to test these systems, the client (us) will kickstart a new process
+// on the server, and try to establish connection to it.
+console.log("making client-to-server request to start remote process,")
+console.log("and connecting to it w/ new websocket")
+
+let wscVPortStatus = "opening"
+// here we attach the "clear to send" function,
+// in this case we aren't going to flowcontrol anything, js buffers are infinite
+// and also impossible to inspect  
+wscVPort.cts = () => { return (wscVPortStatus == "open") }
+// we also have isOpen, similarely simple here, 
+wscVPort.isOpen = () => { return (wscVPortStatus == "open") }
+
+// ok, let's ask to kick a process on the server,
+// in response, we'll get it's IP and Port,
+// then we can start a websocket client to connect there,
+// automated remote-proc. w/ vPort & wss medium,
+// for args, do '/processName.js?args=arg1,arg2'
+jQuery.get('/startLocal/osapSerialBridge.js', (res) => {
+  if (res.includes('OSAP-wss-addr:')) {
+    let addr = res.substring(res.indexOf(':') + 2)
+    if (addr.includes('ws://')) {
+      wscVPortStatus = "opening"
+      // start up, 
+      console.log('starting socket to remote at', addr)
+      let ws = new WebSocket(addr)
+      ws.binaryType = "arraybuffer"
+      // opens, 
+      ws.onopen = (evt) => {
+        wscVPortStatus = "open"
+        // implement rx
+        ws.onmessage = (msg) => {
+          let uint = new Uint8Array(msg.data)
+          wscVPort.receive(uint)
+        }
+        // implement tx 
+        wscVPort.send = (buffer) => {
+          if (LOGPHY) console.log('PHY WSC Send', buffer)
+          ws.send(buffer)
+        }
+      }
+      ws.onerror = (err) => {
+        wscVPortStatus = "closed"
+        console.log('sckt err', err)
+      }
+      ws.onclose = (evt) => {
+        wscVPortStatus = "closed"
+        console.log('sckt closed', evt)
+      }
+    }
+  } else {
+    console.error('remote OSAP not established', res)
+  }
+})
\ No newline at end of file
diff --git a/system/javascript/client/style.css b/system/javascript/client/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..72f524be30d1a4d59ce42ee0d3e236a6daba9b92
--- /dev/null
+++ b/system/javascript/client/style.css
@@ -0,0 +1,202 @@
+html {
+	height: 100%;
+	/*
+	margin: 0px;
+	padding: 0px;
+	*/
+}
+
+body {
+	height: 100%;
+	overflow: hidden;
+	margin: 0px;
+	padding: 0px;
+	font-family: Palatino, serif;
+	font-size: 11px;
+}
+
+#wrapper {
+	overflow: hidden;
+	width: 100%;
+	height: 100%;
+	padding: 0px;
+	margin: 0px;
+	background: #fff;
+	/*background-image:url("background.png");*/
+	/*background-origin: content-box;*/
+	background-image:
+		linear-gradient(rgba(225, 225, 225, .2) 1px, transparent 1px),
+		linear-gradient(90deg, rgba(225, 225, 225, .2) 1px, transparent 1px);
+	background-size: 10px 10px;
+}
+
+.plane {
+	position: absolute;
+	width: 10px;
+	height: 10px;
+}
+
+.pad {
+	position: absolute;
+}
+
+/* svg bs */
+
+.svgcont {
+	position: absolute;
+	overflow: visible;
+	width: 5px;
+	height: 5px;
+}
+
+/* nodes */
+
+.node {
+	position:absolute;
+	background-color: #f5f5f5;
+}
+
+.nodename {
+	font-size: 11px;
+	font-family: Helvetica, sans-serif;
+	width: 100px;
+	padding-left: 5px;
+	padding-top: 5px;
+	transform-origin: 0 0;
+	transform: rotate(-90deg);
+}
+
+.nodenamebus {
+	font-size: 11px;
+	font-family: Helvetica, sans-serif;
+	width: 100px;
+	padding-left: 5px;
+	padding-top: 5px;
+	transform-origin: 0 0;
+	transform: translate(70px);
+}
+
+/* vPorts */
+
+.vPort {
+	position: absolute;
+	background-color: #ebebeb;
+	border-left: 2px solid black;
+	border-right: 2px solid black;
+}
+
+.vPortname {
+	font-size: 11px;
+	font-family: Helvetica, sans-serif;
+	width: 120px;
+	padding-left: 5px;
+	padding-top: 3px;
+	transform-origin: 0 0;
+	transform: rotate(-90deg) translate(-110px, 0px);
+}
+
+/* interactive */
+
+.inputwrap{
+	background-color: #f5f5f5;
+	font-family: 'Courier New', Courier, monospace;
+	font-size: 11px;
+}
+
+.button {
+	background-color: #f5f5f5;
+	font-family: 'Courier New', Courier, monospace;
+	display:flex;
+	justify-content: center;
+	align-items: center;
+	font-size: 11px;
+	text-align: center;
+	padding: 3px;
+}
+
+.btnText {
+	margin: auto;
+	text-align: center;
+	display: flex;
+}
+
+.button:hover{
+	background-color: #ebebeb;
+	cursor: pointer;
+}
+
+.textBlock {
+	background-color: #f5f5f5;
+	font-family: 'Courier New', Courier, monospace;
+	display:flex;
+	justify-content: center;
+	align-items: center;
+	font-size: 11px;
+	text-align: center;
+	padding: 3px;
+}
+
+/* s/o https://brm.io/dat-gui-light-theme/ */
+
+.dg.main.taller-than-window .close-button {
+    border-top: 1px solid #ddd;
+}
+
+.dg.main .close-button {
+    background-color: #ccc;
+}
+
+.dg.main .close-button:hover {
+    background-color: #ddd;
+}
+
+.dg {
+    color: #555;
+    text-shadow: none !important;
+}
+
+.dg.main::-webkit-scrollbar {
+    background: #fafafa;
+}
+
+.dg.main::-webkit-scrollbar-thumb {
+    background: #bbb;
+}
+
+.dg li:not(.folder) {
+    background: #fafafa;
+    border-bottom: 1px solid #ddd;
+}
+
+.dg li.save-row .button {
+    text-shadow: none !important;
+}
+
+.dg li.title {
+    background: #e8e8e8 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;
+}
+
+.dg .cr.function:hover,.dg .cr.boolean:hover {
+    background: #fff;
+}
+
+.dg .c input[type=text] {
+    background: #e9e9e9;
+}
+
+.dg .c input[type=text]:hover {
+    background: #eee;
+}
+
+.dg .c input[type=text]:focus {
+    background: #eee;
+    color: #555;
+}
+
+.dg .c .slider {
+    background: #e9e9e9;
+}
+
+.dg .c .slider:hover {
+    background: #eee;
+}
diff --git a/system/javascript/favicon.ico b/system/javascript/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..b37ae267328392401f2b85880434119912c9db4d
Binary files /dev/null and b/system/javascript/favicon.ico differ
diff --git a/system/javascript/local/osapSerialBridge.js b/system/javascript/local/osapSerialBridge.js
new file mode 100644
index 0000000000000000000000000000000000000000..7849f4d5521612fd2da50311c0ac0e4da4082214
--- /dev/null
+++ b/system/javascript/local/osapSerialBridge.js
@@ -0,0 +1,168 @@
+/*
+osap-usb-bridge.js
+
+osap bridge to firmwarelandia
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// big s/o to https://github.com/standard-things/esm for allowing this
+import OSAP from '../osapjs/core/osap.js'
+import { TS } from '../osapjs/core/ts.js'
+import PK from '../osapjs/core/packets.js'
+
+import WSSPipe from './utes/wssPipe.js'
+import VPortSerial from '../osapjs/vport/vPortSerial.js'
+
+import { SerialPort } from 'serialport'
+
+// we include an osap object - a node
+let osap = new OSAP("local-usb-bridge")
+osap.description = "node featuring wss to client and usbserial cobs connection to hardware"
+
+// -------------------------------------------------------- WSS VPort
+
+let wssVPort = osap.vPort("wssVPort")   // 0
+
+// -------------------------------------------------------- FIFO 
+
+let fifoIn = osap.endpoint("fifoInput")
+let fifoOut = osap.endpoint("fifoOutput")
+fifoOut.setTimeoutLength(60000)
+let fifoLength = 128 
+let fifoBuffer = [] 
+
+// we can attach 'onData' handlers, which fire whenever something is tx'd to us: 
+fifoIn.onData = (data) => {
+  return new Promise((resolve, reject) => {
+    try {
+      let ingestCheck = () => {
+        if(fifoBuffer.length >= fifoLength){
+          setTimeout(ingestCheck, 10)
+        } else {
+          fifoBuffer.push(data)
+          console.log(`>>> fifo ${fifoBuffer.length} / ${fifoLength}`)
+          checkFifoLoop()
+          resolve()
+        }
+      }
+      ingestCheck()
+    } catch (err) {
+      console.error(err)
+    }
+  })
+}
+
+let fifoCheckTimer = null 
+let currentlyAwaiting = false 
+
+let checkFifoLoop = async () => {
+  try {
+    if(currentlyAwaiting == true) return 
+    if(fifoCheckTimer) return 
+    if(fifoBuffer.length > 0){
+      let data = fifoBuffer.shift()
+      if(!data){
+        console.error('que???')
+        return
+      }
+      currentlyAwaiting = true 
+      await fifoOut.write(data, "acked")
+      currentlyAwaiting = false 
+      console.log(`fifo >>> ${fifoBuffer.length} / ${fifoLength}`)
+      if(fifoBuffer.length > 0){
+        fifoCheckTimer = setTimeout(() => {
+          fifoCheckTimer = false 
+          checkFifoLoop()
+        }, 0)
+      } else {
+        fifoCheckTimer = null 
+      }
+    }
+  } catch (err) {
+    console.error(err) 
+  }
+}
+
+
+// then resolves with the connected webSocketServer to us 
+let LOGWSSPHY = false 
+wssVPort.maxSegLength = 16384
+let wssVPortStatus = "opening"
+// here we attach the "clear to send" function,
+// in this case we aren't going to flowcontrol anything, js buffers are infinite
+// and also impossible to inspect  
+wssVPort.cts = () => { return (wssVPortStatus == "open") }
+// we also have isOpen, similarely simple here, 
+wssVPort.isOpen = () => { return (wssVPortStatus == "open") }
+
+WSSPipe.start().then((ws) => {
+  // no loop or init code, 
+  // implement status 
+  wssVPortStatus = "open"
+  // implement rx,
+  ws.onmessage = (msg) => {
+    if (LOGWSSPHY) console.log('PHY WSS Recv')
+    if (LOGWSSPHY) TS.logPacket(msg.data)
+    wssVPort.receive(msg.data)
+  }
+  // implement transmit 
+  wssVPort.send = (buffer) => {
+    if (LOGWSSPHY) console.log('PHY WSS Send')
+    if (LOGWSSPHY) PK.logPacket(buffer)
+    ws.send(buffer)
+  }
+  // local to us, 
+  ws.onerror = (err) => {
+    wssVPortStatus = "closed"
+    console.log('wss error', err)
+  }
+  ws.onclose = (evt) => {
+    wssVPortStatus = "closed"
+    // because this local script is remote-kicked,
+    // we shutdown when the connection is gone
+    console.log('wss closes, exiting')
+    process.exit()
+    // were this a standalone network node, this would not be true
+  }
+})
+
+// -------------------------------------------------------- USB Serial VPort
+
+// we'd like to periodically poke around and find new ports... 
+let pidCandidates = [
+  '801E', '80CB', '8031', '80CD', '800B'
+]
+let activePorts = []
+let portSweeper = () => {
+  SerialPort.list().then((ports) => {
+    for(let port of ports){
+      let cand = pidCandidates.find(elem => elem == port.productId)
+      if(cand && !activePorts.find(elem => elem.portName == port.path)){ 
+        // we have a match, but haven't already opened this port, 
+        console.log(`FOUND desired prt at ${port.path}, launching vport...`)
+        activePorts.push(new VPortSerial(osap, port.path))
+        console.log(activePorts)
+      }
+    }
+    // also... check deadies, 
+    for(let vp of activePorts){
+      if(vp.status == "closed"){
+        console.log(`CLOSED and rming ${vp.portName}`)
+        console.log('at indice...', activePorts.findIndex(elem => elem == vp))
+        activePorts.splice(activePorts.findIndex(elem => elem == vp), 1)
+        console.log(activePorts)
+      }
+    }
+    // set a timeout, 
+    // setTimeout(portSweeper, 500)
+  })
+}
+
+portSweeper()
\ No newline at end of file
diff --git a/system/javascript/local/utes/cobs.js b/system/javascript/local/utes/cobs.js
new file mode 100644
index 0000000000000000000000000000000000000000..af2f7bce399d4371561cb3d89b056d9f8e68a2f5
--- /dev/null
+++ b/system/javascript/local/utes/cobs.js
@@ -0,0 +1,67 @@
+/*
+cobs.js
+
+consistent overhead byte stuffing, wikipedia implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+function cobsEncode(buf) {
+  var dest = [0];
+  // vfpt starts @ 1,
+  var code_ptr = 0;
+  var code = 0x01;
+
+  function finish(incllast) {
+    dest[code_ptr] = code;
+    code_ptr = dest.length;
+    incllast !== false && dest.push(0x00);
+    code = 0x01;
+  }
+
+  for (var i = 0; i < buf.length; i++) {
+    if (buf[i] == 0) {
+      finish();
+    } else {
+      dest.push(buf[i]);
+      code += 1;
+      if (code == 0xFF) {
+        finish();
+      }
+    }
+  }
+  finish(false);
+
+  // close w/ zero
+  dest.push(0x00)
+
+  return Uint8Array.from(dest);
+}
+
+// COBS decode, tailing zero, that was used to delineate this buffer,
+// is assumed to already be chopped, thus the end is the end 
+
+function cobsDecode(buf) {
+  var dest = [];
+  for (var i = 0; i < buf.length;) {
+    var code = buf[i++];
+    for (var j = 1; j < code; j++) {
+      dest.push(buf[i++]);
+    }
+    if (code < 0xFF && i < buf.length) {
+      dest.push(0);
+    }
+  }
+  return Uint8Array.from(dest)
+}
+
+export default {
+  encode: cobsEncode,
+  decode: cobsDecode
+}
diff --git a/system/javascript/local/utes/wssPipe.js b/system/javascript/local/utes/wssPipe.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc20ab0891a224e42accbd7a6fd1822463c665f5
--- /dev/null
+++ b/system/javascript/local/utes/wssPipe.js
@@ -0,0 +1,85 @@
+/*
+wssPipe.js
+
+automated remote-wss-server,
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// so, ideally this is something that *could* get bootstrapped, if we hook it to
+// osap/local file, but could otherwise run standalone...
+// to start, will write here, then wrap in some interface...
+
+// need to know our own ip,
+import os from 'os'
+import WS from 'ws'
+const WebSocketServer = WS.Server
+//const os = require('os')
+//const WebSocketServer = require('ws').Server
+
+// find our addr,
+let wsAddr = ''
+// once we're listening, report our IP:
+let ifaces = os.networkInterfaces()
+// this just logs the processes IP's to the termina
+Object.keys(ifaces).forEach(function(ifname) {
+  var alias = 0;
+
+  ifaces[ifname].forEach(function(iface) {
+    if ('IPv4' !== iface.family) { //} || iface.internal !== false) {
+      // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
+      return;
+    }
+    if (alias >= 1) {
+      console.log('ip: ' /*ifname + ':' + alias,*/ + iface.address) // + `:${port}`);
+      // this single interface has multiple ipv4 addresses
+      // console.log('serving at: ' ifname + ':' + alias + iface.address + `:${port}`);
+    } else {
+      console.log('ip: ' /*ifname + ':' + alias,*/ + iface.address) //+ `:${port}`);
+      wsAddr = iface.address
+      // this interface has only one ipv4 adress
+      //console.log(ifname, iface.address);
+    }
+    ++alias;
+  });
+});
+
+let wsPort = 4040
+let errs = 0
+let startWSS = () => {
+  return new Promise((resolve, reject) => {
+    const wss = new WebSocketServer({ port: wsPort }, () => {
+      console.log(`OSAP-wss-addr: ws://${wsAddr}:${wsPort}`)
+    })
+    // each new connection gets a new socket,
+    wss.on('connection', (ws) => {
+      resolve(ws)
+    })
+    wss.on('error', (err) => {
+      if (err.code == "EADDRINUSE") {
+        console.log('ports in use, next...')
+        wsPort++
+        errs++
+        if (errs > 12) {
+          console.log('too many occupied ports, bailing')
+          process.exit()
+        } else {
+          return(startWSS())
+        }
+      } else {
+        console.log('exiting due to wss err', err)
+        process.exit()
+      }
+    })
+  })
+}
+
+export default {
+  start: startWSS
+}
diff --git a/system/javascript/lpf.js b/system/javascript/lpf.js
new file mode 100644
index 0000000000000000000000000000000000000000..676570fce8165fa771046f326ae44c5f76be6d2b
--- /dev/null
+++ b/system/javascript/lpf.js
@@ -0,0 +1,119 @@
+/*
+clank controller init 
+
+serves client modules, bootstraps local scripts from client ui.
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// new year new bootstrap
+
+const express = require('express')
+const app = express()
+// this include lets us read data out of put requests,
+const bodyparser = require('body-parser')
+// our fs tools,
+const fs = require('fs')
+//const filesys = require('./filesys.js')
+// and we occasionally spawn local pipes (workers)
+const child_process = require('child_process')
+// will use these to figure where tf we are
+let ownIp = ''
+const os = require('os')
+
+// serve everything: https://expressjs.com/en/resources/middleware/serve-static.html
+app.use(express.static(__dirname))
+// accept post bodies as json,
+app.use(bodyparser.json())
+app.use(bodyparser.urlencoded({extended: true}))
+
+// redirect traffic to /client,
+app.get('/', (req, res) => {
+  res.redirect('/client')
+})
+
+// we also want to institute some pipes: this is a holdover for a better system
+// more akin to nautilus, where server-side graphs are manipulated
+// for now, we just want to dive down to a usb port, probably, so this shallow link is OK
+let processes = []
+app.get('/startLocal/:file', (req, res) => {
+  // launches another node instance at this file w/ these args,
+  let args = ''
+  if(req.query.args){
+    args = req.query.args.split(',')
+  }
+  console.log(`attempt to start ${req.params.file} with args ${args}`)
+  // startup, let's spawn,
+  const process = child_process.spawn('node', ['-r', 'esm', `local/${req.params.file}`])
+  // add our own tag,
+  process.fileName = req.params.file
+  let replied = false
+  let pack = ''
+  process.stdout.on('data', (buf) => {
+    // these emerge as buffers,
+    let msg = buf.toString()
+    // can only reply once to xhr req
+    if(msg.includes('OSAP-wss-addr:') && !replied){
+      res.send(msg)
+      replied = true
+    }
+    // ok, dealing w/ newlines
+    pack = pack.concat(msg)
+    let index = pack.indexOf('\n')
+    while(index >= 0){
+      console.log(`${process.fileName} ${process.pid}: ${pack.substring(0, index)}`)
+      pack = pack.slice(index + 1)
+      index = pack.indexOf('\n')
+    }
+  })
+  process.stderr.on('data', (err) => {
+    if(!replied){
+      res.send('err in local script')
+      replied = true
+    }
+    console.log(`${process.fileName} ${process.pid} err:`, err.toString())
+  })
+  process.on('close', (code) => {
+    console.log(`${process.fileName} ${process.pid} closes:`, code)
+    if(!replied){
+      res.send('local process closed')
+      replied = true
+    }
+  })
+  console.log(`started ${process.fileName} w/ pid ${process.pid}`)
+})
+
+// finally, we tell the express server to listen here:
+let port = 8080
+app.listen(port)
+
+// once we're listening, report our IP:
+let ifaces = os.networkInterfaces()
+// this just logs the processes IP's to the termina
+Object.keys(ifaces).forEach(function(ifname) {
+  var alias = 0;
+
+  ifaces[ifname].forEach(function(iface) {
+    if ('IPv4' !== iface.family){//} || iface.internal !== false) {
+      // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
+      return;
+    }
+    ownIp = iface.address
+    if (alias >= 1) {
+      console.log('controller available on: \t' /*ifname + ':' + alias,*/ + iface.address + `:${port}`);
+      // this single interface has multiple ipv4 addresses
+      // console.log('serving at: ' ifname + ':' + alias + iface.address + `:${port}`);
+    } else {
+      console.log('controller available on:\t' /*ifname + ':' + alias,*/ + iface.address + `:${port}`);
+      // this interface has only one ipv4 adress
+      //console.log(ifname, iface.address);
+    }
+    ++alias;
+  });
+});
diff --git a/system/javascript/osapjs/LICENSE.md b/system/javascript/osapjs/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..15a43f130f42e8fdbffc1ff234d86fb920c5cb0e
--- /dev/null
+++ b/system/javascript/osapjs/LICENSE.md
@@ -0,0 +1,4 @@
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the OSAP project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
\ No newline at end of file
diff --git a/system/javascript/osapjs/README.md b/system/javascript/osapjs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6a4cae0e2cbb1cdc2450f2cdb4bb7f663d53a38
--- /dev/null
+++ b/system/javascript/osapjs/README.md
@@ -0,0 +1,5 @@
+## OSAP JavaScript
+
+This is a submodule for the [OSAP](http://osap.tools) project. 
+
+Does JavaScript stuff, should be an `npm` package. 
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/autoPlot.js b/system/javascript/osapjs/client/components/autoPlot.js
new file mode 100644
index 0000000000000000000000000000000000000000..bfde9e3ca4872348686d1118d43ca821bc56511a
--- /dev/null
+++ b/system/javascript/osapjs/client/components/autoPlot.js
@@ -0,0 +1,122 @@
+/*
+autoPlot.js
+
+data splash
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from '../interface/domTools.js'
+import style from '../interface/style.js'
+
+export default function AutoPlot(xPlace, yPlace, xSize, ySize, title, margin = { top: 40, right: 20, bottom: 30, left: 90 }) {
+  let chart = $(`<div>`).get(0)
+  $(chart).css('background-color', style.grey)
+  let uid = `lineChart_${Math.round(Math.random() * 1000)}_uid`
+  $(chart).attr('id', uid)
+  DT.placeField(chart, xSize, ySize, xPlace, yPlace)
+
+  // the data 
+  var datas = []
+  var numToHold = 100
+  this.setHoldCount = (count) => {
+    numToHold = count
+  }
+
+  let yDomain = null
+  this.setYDomain = (min, max) => {
+    yDomain = [min, max]
+  }
+
+  if (!title) margin.top = 20
+
+  var width = xSize - margin.left - margin.right
+  var height = ySize - margin.top - margin.bottom
+  var x = d3.scaleLinear().range([0, width])
+  var y = d3.scaleLinear().range([height, 0])
+  var thesvg = null
+
+  // redraw 
+  this.redraw = () => {
+    var valueline = d3.line()
+      .x(function (d) {
+        return x(d[0])
+      })
+      .y(function (d) {
+        return y(d[1])
+      })
+    // scale
+    x.domain([d3.min(datas, function (d) {
+      return d[0]
+    }), d3.max(datas, function (d) {
+      return d[0];
+    })])
+    if (yDomain) {
+      y.domain(yDomain)
+    } else {
+      y.domain([d3.min(datas, function (d) {
+        return d[1]
+      }), d3.max(datas, function (d) {
+        return d[1];
+      })])
+    }
+    if (thesvg) {
+      d3.select(`#${uid}`).selectAll("*").remove()
+    }
+    thesvg = d3.select(`#${uid}`).append("svg")
+      .attr("width", width + margin.left + margin.right)
+      .attr("height", height + margin.top + margin.bottom)
+      .append("g")
+      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+    // write it?
+    thesvg.append("path")
+      .data([datas])
+      .attr("fill", "none")
+      .attr("stroke", "black")
+      .attr("stroke-width", "4px")
+      .attr("d", valueline)
+    // write the x axis
+    thesvg.append("g")
+      .attr("transform", "translate(0," + height + ")")
+      .call(d3.axisBottom(x))
+    // the y axis
+    thesvg.append("g")
+      .call(d3.axisLeft(y))
+    // the title 
+    if (title) {
+      let info = ""
+      if (datas.length > 0) {
+        info = (datas[datas.length - 1][1]).toFixed(3)
+      }
+      thesvg.append("text")
+        .attr("x", (width / 2))
+        .attr("y", 0 - (margin.top / 2))
+        .attr("text-anchor", "middle")
+        .style("font-size", "12px")
+        .style("font-family", "'Courier New', Courier, monospace")
+        .text(`${title} ${info}`);
+    }
+  }
+  // startup
+  this.redraw()
+  // add new pts 
+  this.pushPt = (pt) => {
+    datas.push(pt)
+    if (datas.length > numToHold) {
+      datas.shift()
+    }
+  }
+  // reset
+  this.reset = () => {
+    datas = []
+    this.redraw()
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/distanceTransform.js b/system/javascript/osapjs/client/components/distanceTransform.js
new file mode 100644
index 0000000000000000000000000000000000000000..f16c2df598ba3c6cbf6c2ef9fe06013a069c2eed
--- /dev/null
+++ b/system/javascript/osapjs/client/components/distanceTransform.js
@@ -0,0 +1,108 @@
+export default function distanceTransform(imageRGBA) {
+    // const ny = imageRGBA.height;
+    // const nx = imageRGBA.width;
+    // var input = new Uint8ClampedArray(imageRGBA.buffer)
+    // var output = new Float32Array(nx*ny)
+
+    //helpers
+    // const distance = (g, x, y, i) => (y-i)*(y-i) + g[i][x]*g[i][x];
+    //
+    // const intersection = (g, x, y0, y1) => (g[y0][x]*g[y0][x] - g[y1][x]*g[y1][x] + y0*y0 - y1*y1)/(2.0*(y0-y1));
+
+    var ny = imageRGBA.height;
+    var nx = imageRGBA.width;
+    var input = imageRGBA.data;
+    var output = new Float32Array(nx * ny);
+
+    function distance(g, x, y, i) {
+        return (y - i) * (y - i) + g[i][x] * g[i][x];
+    }
+
+    function intersection(g, x, y0, y1) {
+        return (
+            (g[y0][x] * g[y0][x] -
+                g[y1][x] * g[y1][x] +
+                y0 * y0 -
+                y1 * y1) /
+            (2.0 * (y0 - y1))
+        );
+    }
+    //
+    // allocate arrays
+    //
+    var g = [];
+    for (var y = 0; y < ny; ++y) g[y] = new Uint32Array(nx);
+    var h = [];
+    for (var y = 0; y < ny; ++y) h[y] = new Uint32Array(nx);
+    var distances = [];
+    for (var y = 0; y < ny; ++y) distances[y] = new Uint32Array(nx);
+    var starts = new Uint32Array(ny);
+    var minimums = new Uint32Array(ny);
+    var d;
+    //
+    // column scan
+    //
+    for (var y = 0; y < ny; ++y) {
+        //
+        // right pass
+        //
+        var closest = -nx;
+        for (var x = 0; x < nx; ++x) {
+            if (input[(ny - 1 - y) * nx * 4 + x * 4 + 0] != 0) {
+                g[y][x] = 0;
+                closest = x;
+            } else g[y][x] = x - closest;
+        }
+        //
+        // left pass
+        //
+        closest = 2 * nx;
+        for (var x = nx - 1; x >= 0; --x) {
+            if (input[(ny - 1 - y) * nx * 4 + x * 4 + 0] != 0) closest = x;
+            else {
+                d = closest - x;
+                if (d < g[y][x]) g[y][x] = d;
+            }
+        }
+    }
+    //
+    // row scan
+    //
+    for (var x = 0; x < nx; ++x) {
+        var segment = 0;
+        starts[0] = 0;
+        minimums[0] = 0;
+        //
+        // down
+        //
+        for (var y = 1; y < ny; ++y) {
+            while (
+                segment >= 0 &&
+                distance(g, x, starts[segment], minimums[segment]) >
+                distance(g, x, starts[segment], y)
+            )
+                segment -= 1;
+            if (segment < 0) {
+                segment = 0;
+                minimums[0] = y;
+            } else {
+                var newstart = 1 + intersection(g, x, minimums[segment], y);
+                if (newstart < ny) {
+                    segment += 1;
+                    minimums[segment] = y;
+                    starts[segment] = newstart;
+                }
+            }
+        }
+        //
+        // up
+        //
+        for (var y = ny - 1; y >= 0; --y) {
+            d = Math.sqrt(distance(g, x, y, minimums[segment]));
+            output[(ny - 1 - y) * nx + x] = d;
+            if (y == starts[segment]) segment -= 1;
+        }
+    }
+
+    return output;
+}
diff --git a/system/javascript/osapjs/client/components/drawSVG.js b/system/javascript/osapjs/client/components/drawSVG.js
new file mode 100644
index 0000000000000000000000000000000000000000..58c7c5876dac9e75fb9fa5d2b99117a2f496c428
--- /dev/null
+++ b/system/javascript/osapjs/client/components/drawSVG.js
@@ -0,0 +1,87 @@
+export default function DrawSVG(svgin, dpi) {
+    // to manipulate full scale:
+    let vcanvas = document.createElement('canvas')
+
+    // ... getsize, draw, fill ?
+    let getSize = (svg) => {
+        // where 'svg' is some string, we return width, height, units
+        let i = svg.indexOf("width")
+        if (i == -1) {
+            return ({
+                width: 1,
+                height: 1,
+                units: 90
+            })
+        } else {
+            var i1 = svg.indexOf("\"", i + 1)
+            var i2 = svg.indexOf("\"", i1 + 1)
+            var width = svg.substring(i1 + 1, i2)
+            i = svg.indexOf("height")
+            i1 = svg.indexOf("\"", i + 1)
+            i2 = svg.indexOf("\"", i1 + 1)
+            var height = svg.substring(i1 + 1, i2)
+            let ih = svg.indexOf("height")
+            let units = 0
+            if (width.indexOf("px") != -1) {
+                width = width.slice(0, -2)
+                height = height.slice(0, -2)
+                units = 90
+            } else if (width.indexOf("mm") != -1) {
+                width = width.slice(0, -2)
+                height = height.slice(0, -2)
+                units = 25.4
+            } else if (width.indexOf("cm") != -1) {
+                width = width.slice(0, -2)
+                height = height.slice(0, -2)
+                units = 2.54
+            } else if (width.indexOf("in") != -1) {
+                width = width.slice(0, -2)
+                height = height.slice(0, -2)
+                units = 1
+            } else {
+                units = 90
+            }
+            return ({
+                width: width,
+                height: height,
+                units: units
+            })
+        }
+    }
+
+    return new Promise((resolve, reject) => {
+        let loadImageBase64 = (svg, size) => {
+            // btoa converts str. to base 64
+            let src = "data:image/svg+xml;base64," + window.btoa(svg)
+            let img = new Image()
+            img.setAttribute('src', src)
+            //img.src = 'data:image/svg+xml;utf8,' + svg
+            img.onload = () => {
+                let height = size.height * dpi / size.units
+                let width = size.width * dpi / size.units
+                // new vcanvas always,
+                vcanvas = document.createElement('canvas')
+                console.log(width, height)
+                vcanvas.width = width
+                vcanvas.height = height
+                let ctx = vcanvas.getContext('2d')
+                ctx.drawImage(img, 0, 0, width, height)
+                let imgData = ctx.getImageData(0, 0, vcanvas.width, vcanvas.height)
+                //let width = vcanvas.width * 24.5 / dpi
+                resolve({
+                    imgdata: imgData,
+                    width: vcanvas.width * 24.5 / dpi // actual size, not pixels  
+                })
+            }
+            img.onerror = (err) => {
+                reject(err)
+            }
+        }
+        // run,
+        let size = getSize(svgin)
+        console.warn('have size', size)
+        size.width = parseFloat(size.width).toString()
+        size.height = parseFloat(size.height).toString()
+        loadImageBase64(svgin, size)
+    })
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/gCodePanel.js b/system/javascript/osapjs/client/components/gCodePanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..313b13598ead4f911ed56cd9c2103b7105c6af69
--- /dev/null
+++ b/system/javascript/osapjs/client/components/gCodePanel.js
@@ -0,0 +1,437 @@
+/*
+gCodePanel.js
+
+input gcodes 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+/*
+notes on this thing
+
+this is pretty straightforward at the moment, it'll read small gcodes
+i.e. those used to mill circuits. for larger files, like 3DP files,
+reading times / manipulating large streams of texts needs to be handled 
+more intelligently, i.e. not rendering the whole file into the 'incoming' body. 
+*/
+
+'use strict'
+
+import DT from '../interface/domTools.js'
+import { Button, TextBlock, TextInput } from '../interface/basics.js'
+import TIME from '../../core/time.js'
+
+function GCodePanel(xPlace, yPlace, width, machine, hotend) {
+  // some hack padding correction 
+  width -= 3
+  // text box for elements that have been handled, 
+  // previous gcodes thru 
+  let previously = $('<textarea>').addClass('inputwrap')
+    .attr('wrap', 'off')
+    .attr('readonly', 'true')
+    .get(0)
+  DT.placeField(previously, width, 207, xPlace, yPlace)
+  // to load files, 
+  let loadBtn = $('<input type = "file">')
+    .on('change', (evt) => {
+      let reader = new FileReader()
+      reader.onload = () => {
+        let text = reader.result
+        console.log(`loaded file with len ${text.length}`)
+        gCodeIncoming = text
+        incoming.value = text 
+      }
+      reader.readAsText(evt.target.files[0])
+      console.log('load', evt.target.files[0])
+    })
+    .get(0)
+  // load 
+  DT.placeField(loadBtn, width, 20, xPlace, yPlace += 220)
+  // run / pause ... >, ||, 
+  let runBtn = new Button(xPlace, yPlace += 30, 54, 14, '>')
+  runBtn.onClick(() => {
+    if (running) {
+      this.pause()
+    } else {
+      this.start()
+    }
+  })
+  // one line, >|
+  //  let onceBtn = new Button(xPlace + 60, yPlace, 44, 14, '>|')
+  // some status display:
+  let status = new TextBlock(xPlace, yPlace += 30, width - 3, 24, 'paused')
+  // incoming gcodes 
+  let incoming = $('<textarea>').addClass('inputwrap')
+    .attr('wrap', 'off')
+    .get(0)
+  DT.placeField(incoming, width, 460, xPlace, yPlace += 30 + 10)
+
+  // load files w/ 
+  this.loadServerFile = (path) => {
+    this.pause()
+    return new Promise((resolve, reject) => {
+      getServerFile(path).then((res) => {
+        this.loadString(res)
+        resolve()
+      }).catch((err) => {
+        reject(err)
+      })
+    })
+  }
+
+  // load w/ string:
+  let gCodeIncoming = ""
+  this.loadString = (str) => {
+    this.pause()
+    incoming.value = str
+    gCodeIncoming = str 
+  }
+
+  // running, or not: some state:
+  let running = false
+
+  // we should have a basic API, like:
+  this.pause = () => {
+    running = false
+    runBtn.grey(">")
+    status.grey('paused')
+  }
+
+  // begins the loop, should resolve or throw error so long as thing is running ? 
+  this.start = async () => {
+    running = true
+    runBtn.green("||")
+    status.green('starting')
+    while(running){
+      try {
+        let completedLine = await feedNext()
+        if(completedLine == "EOF"){
+          console.log("END, awaitin no motion...")
+          await machine.motion.awaitMotionEnd()
+          running = false 
+        } else {
+          //console.log(`done: ${completedLine}`)
+        }
+      } catch (err) {
+        console.error(err)
+        this.pause()
+        throw err
+      }
+    }
+  }
+
+  // feeds one line, resolves when line is complete: 
+  let feedNext = () => {
+    return new Promise((resolve, reject) => {
+      // parse substring of file on next newline, 
+      let eol = gCodeIncoming.indexOf('\n') + 1
+      // if end of file & no new-line terminating, 
+      if (eol == 0) eol = gCodeIncoming.length
+      // get the new thing, 
+      let line = gCodeIncoming.substring(0, eol)
+      // should check if is end of file 
+      if (gCodeIncoming.length == 0) {
+        resolve("EOF")
+        return
+      }
+      // otherwise parse 
+      parse(line).then(() => {
+        // success, clear and add to prev 
+        //previously.value += line
+        //previously.scrollTop = previously.scrollHeight
+        //console.log('completed', line)
+        resolve(line)
+      }).catch((err) => {
+        // failure, backup 
+        console.error(`error feeding gcode '${line}'`, err)
+        status.red()
+        status.setHTML(`error feeding line, see console:<br>${line}`)
+        gCodeIncoming = line.concat(gCodeIncoming)
+        reject()
+      })
+      gCodeIncoming = gCodeIncoming.substring(eol)
+    })
+  }
+
+  // the actual gcode parsing, 
+  let axesString = "X, Y, Z, E"
+  let axes = pullAxes(axesString)
+  let position = {}
+  for (let axis of axes) {
+    position[axis] = 0.0
+  }
+  let feedRates = { // in mm/min: defaults if not set 
+    G00: 600, // rapids
+    G01: 60 // feeds 
+  }
+  let feedMode = 'G01'
+  let posConvert = 1 // to swap mm / inches if G20 or G21 
+  let feedConvert = 1 / 60 // to swap units/s and units/inch ... 
+  // here we go: 
+  let parse = async (line) => {
+    if (line.length == 0) {
+      return
+    }
+    let move = false
+    let words = stripComments(line).match(re) || []
+    if (words.length < 1) return
+    // single feed: sets all feedrates 
+    if (words[0].includes('F')) {
+      let feed = parseFloat(words[0].substring(1))
+      if (Number.isNaN(feed)) {
+        console.error('NaN for GCode Parse Feed')
+      } else {
+        for (let f in feedRates) {
+          feedRates[f] = feed
+        }
+      }
+      return
+    } // end lonely F     
+    // do normal pickings 
+    switch (words[0]) {
+      case 'T0':
+        await machine.getTool('no tool')
+        break;
+      case 'T1':
+        await machine.getTool('eraser')
+        break;
+      case 'T2':
+        await machine.getTool('pencil')
+        break;
+      case 'G20':
+        posConvert = 25.4
+        feedConvert = 25.4 / 60   // mm/min to mm/sec 
+        break;
+      case 'G21':
+        posConvert = 1
+        feedConvert = 1 / 60      // at gcode interface we swap mm/min to mm/sec
+        break;
+      case 'G00':
+      case 'G0':
+        feedMode = 'G00'
+        let g0move = gMove(words)
+        status.setText(line)
+        // some of these *just* set feedrate, 
+        if (g0move.rateOnly) return
+        await machine.motion.addMoveToQueue(g0move)
+        break;
+      case 'G01':
+      case 'G1':
+        feedMode = 'G01'
+        let g1move = gMove(words)
+        status.setText(line)
+        if (g1move.rateOnly) return
+        await machine.motion.addMoveToQueue(g1move)
+        break;
+      case 'G04':
+      case 'G4':
+        await machine.motion.awaitMotionEnd()
+        await TIME.delay(parseInt(words[1].slice(1)))
+        break;
+      case 'G28':
+        console.warn('ignoring G28 home')
+        break;
+      case 'G80':
+        console.warn('ignoring G80 mesh bed levelling')
+        break;
+      case 'G90':
+        // 'use absolute coordinates'
+        console.warn('ignoring G90 use absolute coordinates')
+        break;
+      case 'G92':
+        console.warn('ignoring G92 set pos')
+        break;
+      case 'M03':
+        let rpm = words[1].substring(1)
+        if (Number.isNaN(rpm)) {
+          rpm = 0
+          console.error('bad RPM parse')
+        }
+        // HERE
+        console.log('would await end, then set rpm', rpm)
+        //await vm.motion.awaitMotionEnd()
+        //await this.spindleOut.send(rpm)
+        break;
+      case 'M05':
+        // HERE 
+        console.log('would await end, then set rpm 0')
+        //await this.awaitMotionEnd.send()
+        //await this.spindleOut.send(0)
+        break;
+      case 'M83':
+        // use relative extruder mode,
+        console.warn('ignoring M83 use rel extrude')
+        break;
+      case 'M104': {
+        // set extruder temp,
+        let temp = parseFloat(words[1].substring(1))
+        console.log('would set extruder temp', temp)
+        //await this.extruderTempOut.send(temp)
+        break;
+      }
+      case 'M106': {
+        // set fan speed 
+        let speed = parseFloat(words[1].substring(1)) / 255
+        await hotend.setPCF(speed)
+        console.log('set fan speed', speed)
+        break;
+      }
+      case 'M140': {
+        // set bed temp,
+        let temp = parseFloat(words[1].substring(1))
+        // HERE
+        console.log('would await bed temp', temp)
+        //await this.bedTempOut.send(temp)
+        break;
+      }
+      case 'M109': {
+        // await extruder temp, 
+        let temp = parseFloat(words[1].substring(1))
+        // HERE 
+        console.log('would await extruder temp', temp)
+        //await this.awaitExtruderTemp.send(temp)
+        break;
+      }
+      case 'M190': {
+        // await bed temp 
+        let temp = parseFloat(words[1].substring(1))
+        // HERE 
+        console.log('would await bed temp', temp)
+        //await this.awaitBedTemp.send(temp)
+        break;
+      }
+      case 'M201':
+      // these codes set max accelerations... 
+      case 'M203':
+      // these set maximum rates 
+      case 'M204':
+      // set printing (P arg) and traversing (T arg) accelerations
+      case 'M205':
+      // set jerk rates 
+      case 'M107':
+      // sets the print fan off, 
+      case 'M221':
+      // sets extruder override percentage 
+      case 'M900':
+      // sets extrusion pressure lookahead parameters... 
+      default:
+        console.warn('ignoring GCode', line)
+        break;
+    } // end first word switch     
+  } // end parse 
+
+  let gMove = (words) => {
+    // to check for E-alone moves, 
+    let includesE, includesX, includesY, includesZ, includesF = false;
+    for (let word of words) {
+      if (word.includes('E')) includesE = true;
+      if (word.includes('X')) includesX = true;
+      if (word.includes('Y')) includesY = true;
+      if (word.includes('Z')) includesZ = true;
+      if (word.includes('F')) includesF = true;
+    }
+    if (includesE && (!includesX && !includesY && !includesZ)) {
+      // turns out, this works OK... 
+      console.warn('E-Only G Code')
+    }
+    // always reset e-position to zero, 
+    // this one isn't stateful, is incremental: 
+    position.E = 0
+    // now load in posns, 
+    for (let word of words) {
+      for (let axis of axes) {
+        if (word.includes(axis)) {
+          let pos = parseFloat(word.substring(1))
+          if (Number.isNaN(pos)) {
+            console.error('NaN for GCode Parse Position')
+          } else {
+            position[axis] = pos
+          }
+        }
+      } // end check axis in word, 
+      if (word.includes('F')) {
+        let feed = parseFloat(word.substring(1))
+        if (Number.isNaN(feed)) {
+          console.error('NaN for GCode Parse Feed')
+        } else {
+          feedRates[feedMode] = feed
+        }
+      }
+    } // end for-words 
+    // output the move, 
+    let move = { position: {}, rate: feedRates[feedMode] * feedConvert }
+    for (let axis of axes) {
+      move.position[axis] = position[axis] * posConvert
+    }
+    // or, // check for rate-only move, 
+    if (includesF && !includesX && !includesY && !includesZ && !includesE) {
+      console.warn('F-Only G Code')
+      move.rateOnly = true
+    } else {
+      move.rateOnly = false
+    }
+    return move
+  }
+}
+
+// reference:
+// spy from https://github.com/cncjs/gcode-parser/blob/master/src/index.js thx 
+/*
+G00:        move at rapids speed 
+G01:        move at last G01 F<num>
+G04 P<num>:  dwell for P milliseconds or X seconds 
+G20:        set coordinates to inches 
+G21:        set coordinates to mm 
+G28:        do homing routine 
+G90:        positions are absolute w/r/t zero 
+G91:        positions are incremenetal w/r/t last moves 
+G94:        feedrates are per minute 
+*/
+/*
+F<num>:     set feedrate for modal G 
+M03 S<num>: set clockwise rotation 
+M04 S<num>: set counter-clockwise rotation 
+M05:        stop spindle 
+M83:        use extruder relative motion 
+*/
+
+export { GCodePanel }
+
+// lifted from https://github.com/cncjs/gcode-parser/blob/master/src/index.js
+const stripComments = (() => {
+  const re1 = new RegExp(/\s*\([^\)]*\)/g); // Remove anything inside the parentheses
+  const re2 = new RegExp(/\s*;.*/g); // Remove anything after a semi-colon to the end of the line, including preceding spaces
+  const re3 = new RegExp(/\s+/g);
+  return (line => line.replace(re1, '').replace(re2, '').replace(re3, ''));
+})()
+const re = /(%.*)|({.*)|((?:\$\$)|(?:\$[a-zA-Z0-9#]*))|([a-zA-Z][0-9\+\-\.]+)|(\*[0-9]+)/igm
+
+let pullAxes = (str) => {
+  const whiteSpace = new RegExp(/\s*/g)
+  str = str.replace(whiteSpace, '')
+  return str.split(',')
+}
+
+// startup with demo gcode, for testing 
+let getServerFile = (file) => {
+  return new Promise((resolve, reject) => {
+    if (!file) {
+      reject('no startup file, ok')
+      return
+    }
+    $.ajax({
+      type: "GET",
+      url: file,
+      error: function () { reject(`req for ${file} fails`) },
+      success: function (xhr, statusText) {
+        resolve(xhr)
+      }
+    })
+  })
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/gerberConverter.js b/system/javascript/osapjs/client/components/gerberConverter.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c3918cf8eb15c8e9c4cbbc5906793c9581b6a0a
--- /dev/null
+++ b/system/javascript/osapjs/client/components/gerberConverter.js
@@ -0,0 +1,17 @@
+// ute ! 
+
+export default function gerberConverter(input, options){
+  return new Promise((resolve, reject) => {
+    if (!options) options = { encoding: 'utf8' }
+    let converter = gerberToSvg(input, options, (err, svg) => {
+      if (err) {
+        reject(err)
+      } else {
+        // the SVG has .attr for viewbox, anchors, units, etc, 
+        // see https://github.com/tracespace/tracespace/blob/main/packages/gerber-to-svg/API.md#output 
+        converter.svg = svg 
+        resolve(converter)
+      }
+    })
+  })
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/gerberToImage.js b/system/javascript/osapjs/client/components/gerberToImage.js
new file mode 100644
index 0000000000000000000000000000000000000000..e1657433bcfa0ca84c57f611d739722719909fcb
--- /dev/null
+++ b/system/javascript/osapjs/client/components/gerberToImage.js
@@ -0,0 +1,64 @@
+// this is a total stub that I'm bottling out here: it was in the clankiteClient.js... 
+
+let svgImageLoader = (source) => {
+  return new Promise((resolve, reject) => {
+    let image = new Image()
+    image.onload = () => {
+      resolve(image)
+    }
+    image.onerror = (err) => {
+      reject(`failed to load image with source ${source}`)
+    }
+    image.src = source
+  })
+}
+
+let gerberTestCode = async () => {
+  try {
+    // get the gerber as a utf-8 file, 
+    let gerb = await GetFile(`save/testGerber/fab-step/GerberFiles/copper_top.gbr`)
+    // console.log("gerb", gerb)
+    let width = 200
+    let height = 200
+    // make it into an svg / this library's interpretation 
+    let gerbSVG = await gerberConverter(gerb)
+    console.log("converted...", gerbSVG)
+    // now... we have gerbRep.layer which is an array of svg elements, 
+    // so we should be able to make an svg canvas / thing in the dom and throw these in ? 
+    let dom = $('.plane').get(0)
+    let cont = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
+    $(dom).append(gerbSVG)
+    // we make a blob of the *raw svg string*
+    let blob = new Blob([gerbSVG], { type: 'image/svg+xml;charset=utf-8' })
+    console.log('have a blob...', blob)
+    // url of the blob, 
+    let blobURL = (window.URL || window.webkitURL || window).createObjectURL(blob);
+    console.log('have blob url...', blobURL)
+    // get an image object, 
+    let image = await svgImageLoader(blobURL)
+    console.log('awaited image...', image)
+    let canvas = document.createElement('canvas')
+    canvas.width = width
+    canvas.height = height
+    let context = canvas.getContext('2d');   // draw image in canvas starting left-0 , top - 0     
+    context.drawImage(image, 0, 0, width, height);
+    console.log('into context', context)
+    $(dom).append(canvas)
+    // can we get the image data...
+    let imageData = context.getImageData(0, 0, width, height)
+    console.log(imageData)
+    let prePath = await ImgToPath2D(imageData, width, 0, 10, -1, 0.5)
+    console.log(prePath)
+    let path = []
+    for (let point of prePath) {
+      // transform(point, true)
+      path.push({
+        target: point,
+        rate: 200
+      })
+    }
+    return path
+  } catch (err) {
+    console.error(err)
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/img2path.js b/system/javascript/osapjs/client/components/img2path.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2b92120a9c2d887fb1ae0843c15f47f0a2eeebf
--- /dev/null
+++ b/system/javascript/osapjs/client/components/img2path.js
@@ -0,0 +1,940 @@
+/*
+threshold, edges, orient, vectorize, z-clearances and cuts,
+
+Jake Read at the Center for Bits and Atoms with Neil Gershenfeld and Leo McElroy
+(c) Massachusetts Institute of Technology 2019
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the squidworks and cuttlefish projects.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import distanceTransform from './distancetransform.js'
+import imgOffset from './imgoffset.js'
+
+export default function ImgToPath2D(args) {
+  let imgdata = args.imageData
+  let width = args.realWidth
+  let offset = args.toolOffset
+  let zUp = args.zUp
+  let zDown = args.zDown
+  let passDepth = args.passDepth
+  // ok, we'll work on inputs, this will take some time,
+  // we'll store outputs, wait for them to clear
+  let store = null
+  console.log(`thresholding...`)
+  let threshold = thresholdRGBA(imgdata, 0.5)
+  console.log(`distance tf...`)
+  let distance = distanceTransform(threshold)
+  let pixelOffset = (imgdata.width / width) * offset
+  console.log(`pixel offset is ${pixelOffset}, offsetting...`)
+  let offsetImg = imgOffset(distance, pixelOffset, imgdata.width, imgdata.height)
+  console.log(`edge detecting...`)
+  let edges = edgeDetectHelper(offsetImg)
+  console.log(`edge orienting...`)
+  let oriented = orientEdgesHelper(edges)
+  // ahn worker for this one, 
+  console.log(`vectorizing...`)
+  let blob = new Blob(["(" + vectorWorker.toString() + "())"])
+  let url = window.URL.createObjectURL(blob)
+  let worker = new Worker(url)
+  let result = null
+  worker.onmessage = (e) => {
+    store = e.data
+    worker.terminate()
+    // now, this has done path-to-array in pixel space. we want those in
+    // to be related to the width of our image,
+    // and we want to add those jog features,
+    result = pixToPath(e.data, imgdata.width, width, zUp, zDown, passDepth, args.feedRate, args.jogRate)
+  }
+  // lazy, 
+  worker.postMessage(oriented)
+  return new Promise((resolve, reject) => {
+    let check = () => {
+      if (!result) {
+        setTimeout(check, 10)
+      } else {
+        console.log('img2path done...')
+        resolve(result)
+      }
+    }
+    check()
+  })
+}
+
+const pixToPath = (path, pwidth, mmwidth, zu, zd, pd, feed, jog) => {
+  // RIP oldboy
+  let newPath = []
+  // expansion,
+  let scale = mmwidth / pwidth
+  // first move is to 0,0,clearance
+  newPath.push({
+    target: [0, 0, zu],
+    rate: jog
+  })
+  // flatten, adding z-moves
+  for (let leg of path) {
+    // start each leg up top, above the first point,
+    newPath.push({
+      target: [scale * leg[0][0], scale * leg[0][1], zu],
+      rate: jog,
+    })
+    // pass depth should always be negative... 
+    pd = (pd > 0) ? - pd : pd;
+    // fill in first passes, 
+    if (pd && Math.abs(zd / pd) > 1) {
+      let passes = Math.abs(Math.ceil(zd / pd))
+      console.warn(`making ${passes} passes`)
+      for (let p = 0; p < passes - 1; p++) {
+        for (let point of leg) {
+          newPath.push({
+            target: [scale * point[0], scale * point[1], pd * (p + 1)],
+            rate: feed
+          })
+        }
+      }
+    }
+    // fill in last (or only) pass 
+    for (let point of leg) {
+      newPath.push({
+        target: [scale * point[0], scale * point[1], zd],
+        rate: feed
+      })
+    }
+    // and the lift, to tail
+    let last = leg[leg.length - 1]
+    newPath.push({
+      target: [scale * last[0], scale * last[1], zu],
+      rate: jog
+    })
+  }
+  return newPath
+}
+
+// Helper Functions
+const thresholdRGBA = (imageRGBA, threshold) => {
+  const w = imageRGBA.width;
+  const h = imageRGBA.height;
+  const buf = imageRGBA.data;
+  const t = threshold;
+
+  let r, g, b, a, i;
+  for (var row = 0; row < h; ++row) {
+    for (var col = 0; col < w; ++col) {
+      r = buf[(h - 1 - row) * w * 4 + col * 4 + 0];
+      g = buf[(h - 1 - row) * w * 4 + col * 4 + 1];
+      b = buf[(h - 1 - row) * w * 4 + col * 4 + 2];
+      a = buf[(h - 1 - row) * w * 4 + col * 4 + 3];
+      i = (r + g + b) / (3 * 255);
+
+      let val;
+      if (a === 0) {
+        val = 255;
+      } else if (i > t) {
+        val = 255;
+      } else {
+        val = 0;
+      }
+
+      buf[(h - 1 - row) * w * 4 + col * 4 + 0] = val;
+      buf[(h - 1 - row) * w * 4 + col * 4 + 1] = val;
+      buf[(h - 1 - row) * w * 4 + col * 4 + 2] = val;
+      buf[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    }
+  }
+
+  const imgdata = new ImageData(buf, w, h);
+
+  return imgdata;
+};
+
+const edgeDetectHelper = (imageRGBA) => {
+  var h = imageRGBA.height;
+  var w = imageRGBA.width;
+  var input = imageRGBA.data;
+  var output = new Uint8ClampedArray(h * w * 4);
+  var i00, i0m, i0p, im0, ip0, imm, imp, ipm, ipp, row, col;
+  //
+  // find edges - interior
+  //
+  for (row = 1; row < h - 1; ++row) {
+    for (col = 1; col < w - 1; ++col) {
+      i00 =
+        input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+        input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+        input[(h - 1 - row) * w * 4 + col * 4 + 2];
+      i0p =
+        input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+        input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+        input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+      ip0 =
+        input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+        input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+        input[(h - 2 - row) * w * 4 + col * 4 + 2];
+      ipp =
+        input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 0] +
+        input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 1] +
+        input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 2];
+      i0m =
+        input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+        input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+        input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+      im0 =
+        input[(h - row) * w * 4 + col * 4 + 0] +
+        input[(h - row) * w * 4 + col * 4 + 1] +
+        input[(h - row) * w * 4 + col * 4 + 2];
+      imm =
+        input[(h - row) * w * 4 + (col - 1) * 4 + 0] +
+        input[(h - row) * w * 4 + (col - 1) * 4 + 1] +
+        input[(h - row) * w * 4 + (col - 1) * 4 + 2];
+      imp =
+        input[(h - row) * w * 4 + (col + 1) * 4 + 0] +
+        input[(h - row) * w * 4 + (col + 1) * 4 + 1] +
+        input[(h - row) * w * 4 + (col + 1) * 4 + 2];
+      ipm =
+        input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 0] +
+        input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 1] +
+        input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 2];
+      if (
+        i00 != i0p ||
+        i00 != ip0 ||
+        i00 != ipp ||
+        i00 != i0m ||
+        i00 != im0 ||
+        i00 != imm ||
+        i00 != imp ||
+        i00 != ipm
+      ) {
+        output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+        output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+      } else if (i00 == 0) {
+        output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+        output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+      } else {
+        output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+        output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+        output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+      }
+    }
+  }
+  //
+  // left and right edges
+  //
+  for (row = 1; row < h - 1; ++row) {
+    col = w - 1;
+    i00 =
+      input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 2];
+    i0m =
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+    imm =
+      input[(h - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - row) * w * 4 + (col - 1) * 4 + 2];
+    ipm =
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 2];
+    im0 =
+      input[(h - row) * w * 4 + col * 4 + 0] +
+      input[(h - row) * w * 4 + col * 4 + 1] +
+      input[(h - row) * w * 4 + col * 4 + 2];
+    ip0 =
+      input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 2];
+    if (i00 != i0m || i00 != ip0 || i00 != ipm || i00 != im0 || i00 != imm) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else if (i00 == 0) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    }
+    col = 0;
+    i00 =
+      input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 2];
+    i0p =
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+    imp =
+      input[(h - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - row) * w * 4 + (col + 1) * 4 + 2];
+    ipp =
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 2];
+    im0 =
+      input[(h - row) * w * 4 + col * 4 + 0] +
+      input[(h - row) * w * 4 + col * 4 + 1] +
+      input[(h - row) * w * 4 + col * 4 + 2];
+    ip0 =
+      input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 2];
+    if (i00 != i0p || i00 != ip0 || i00 != ipp || i00 != im0 || i00 != imp) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else if (i00 == 0) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    }
+  }
+  //
+  // top and bottom edges
+  //
+  for (col = 1; col < w - 1; ++col) {
+    row = h - 1;
+    i00 =
+      input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 2];
+    i0m =
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+    i0p =
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+    imm =
+      input[(h - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - row) * w * 4 + (col - 1) * 4 + 2];
+    im0 =
+      input[(h - row) * w * 4 + col * 4 + 0] +
+      input[(h - row) * w * 4 + col * 4 + 1] +
+      input[(h - row) * w * 4 + col * 4 + 2];
+    imp =
+      input[(h - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - row) * w * 4 + (col + 1) * 4 + 2];
+    if (i00 != i0m || i00 != i0p || i00 != imm || i00 != im0 || i00 != imp) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else if (i00 == 0) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    }
+    row = 0;
+    i00 =
+      input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + col * 4 + 2];
+    i0m =
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+    i0p =
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+    ipm =
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 2];
+    ip0 =
+      input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + col * 4 + 2];
+    ipp =
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 0] +
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 1] +
+      input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 2];
+    if (i00 != i0m || i00 != i0p || i00 != ipm || i00 != ip0 || i00 != ipp) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else if (i00 == 0) {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    } else {
+      output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+      output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+    }
+  }
+  //
+  // corners
+  //
+  row = 0;
+  col = 0;
+  i00 =
+    input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 2];
+  i0p =
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+  ip0 =
+    input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 2 - row) * w * 4 + col * 4 + 2];
+  ipp =
+    input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 0] +
+    input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 1] +
+    input[(h - 2 - row) * w * 4 + (col + 1) * 4 + 2];
+  if (i00 != i0p || i00 != ip0 || i00 != ipp) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else if (i00 == 0) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  }
+  row = 0;
+  col = w - 1;
+  i00 =
+    input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 2];
+  i0m =
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+  ip0 =
+    input[(h - 2 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 2 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 2 - row) * w * 4 + col * 4 + 2];
+  ipm =
+    input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 0] +
+    input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 1] +
+    input[(h - 2 - row) * w * 4 + (col - 1) * 4 + 2];
+  if (i00 != i0m || i00 != ip0 || i00 != ipm) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else if (i00 == 0) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  }
+  row = h - 1;
+  col = 0;
+  i00 =
+    input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 2];
+  i0p =
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + (col + 1) * 4 + 2];
+  im0 =
+    input[(h - row) * w * 4 + col * 4 + 0] +
+    input[(h - row) * w * 4 + col * 4 + 1] +
+    input[(h - row) * w * 4 + col * 4 + 2];
+  imp =
+    input[(h - row) * w * 4 + (col + 1) * 4 + 0] +
+    input[(h - row) * w * 4 + (col + 1) * 4 + 1] +
+    input[(h - row) * w * 4 + (col + 1) * 4 + 2];
+  if (i00 != i0p || i00 != im0 || i00 != imp) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else if (i00 == 0) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  }
+  row = h - 1;
+  col = w - 1;
+  i00 =
+    input[(h - 1 - row) * w * 4 + col * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + col * 4 + 2];
+  i0m =
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 0] +
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 1] +
+    input[(h - 1 - row) * w * 4 + (col - 1) * 4 + 2];
+  im0 =
+    input[(h - row) * w * 4 + col * 4 + 0] +
+    input[(h - row) * w * 4 + col * 4 + 1] +
+    input[(h - row) * w * 4 + col * 4 + 2];
+  imm =
+    input[(h - row) * w * 4 + (col - 1) * 4 + 0] +
+    input[(h - row) * w * 4 + (col - 1) * 4 + 1] +
+    input[(h - row) * w * 4 + (col - 1) * 4 + 2];
+  if (i00 != i0m || i00 != im0 || i00 != imm) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else if (i00 == 0) {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  } else {
+    output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+    output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+  }
+
+  const imgData = new ImageData(output, w, h);
+
+  return imgData;
+};
+
+const orientEdgesHelper = imageRGBA => {
+  var h = imageRGBA.height;
+  var w = imageRGBA.width;
+  var input = imageRGBA.data;
+  var output = new Uint8ClampedArray(h * w * 4);
+  var row, col;
+  var boundary = 0;
+  var interior = 1;
+  var exterior = 2;
+  var alpha = 3;
+  var northsouth = 0;
+  var north = 128;
+  var south = 64;
+  var eastwest = 1;
+  var east = 128;
+  var west = 64;
+  var startstop = 2;
+  var start = 128;
+  var stop = 64;
+  //
+  // orient body states
+  //
+  for (row = 1; row < h - 1; ++row) {
+    for (col = 1; col < w - 1; ++col) {
+      output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+      output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+      if (input[(h - 1 - row) * w * 4 + col * 4 + boundary] != 0) {
+        if (
+          input[(h - 1 - (row + 1)) * w * 4 + col * 4 + boundary] != 0 &&
+          (input[(h - 1 - row) * w * 4 + (col + 1) * 4 + interior] != 0 ||
+            input[(h - 1 - (row + 1)) * w * 4 + (col + 1) * 4 + interior] !=
+            0)
+        )
+          output[(h - 1 - row) * w * 4 + col * 4 + northsouth] |= north;
+        if (
+          input[(h - 1 - (row - 1)) * w * 4 + col * 4 + boundary] != 0 &&
+          (input[(h - 1 - row) * w * 4 + (col - 1) * 4 + interior] != 0 ||
+            input[(h - 1 - (row - 1)) * w * 4 + (col - 1) * 4 + interior] !=
+            0)
+        )
+          output[(h - 1 - row) * w * 4 + col * 4 + northsouth] |= south;
+        if (
+          input[(h - 1 - row) * w * 4 + (col + 1) * 4 + boundary] != 0 &&
+          (input[(h - 1 - (row - 1)) * w * 4 + col * 4 + interior] != 0 ||
+            input[(h - 1 - (row - 1)) * w * 4 + (col + 1) * 4 + interior] !=
+            0)
+        )
+          output[(h - 1 - row) * w * 4 + col * 4 + eastwest] |= east;
+        if (
+          input[(h - 1 - row) * w * 4 + (col - 1) * 4 + boundary] != 0 &&
+          (input[(h - 1 - (row + 1)) * w * 4 + col * 4 + interior] != 0 ||
+            input[(h - 1 - (row + 1)) * w * 4 + (col - 1) * 4 + interior] !=
+            0)
+        )
+          output[(h - 1 - row) * w * 4 + col * 4 + eastwest] |= west;
+      }
+    }
+  }
+  //
+  // orient edge states
+  //
+  for (col = 1; col < w - 1; ++col) {
+    row = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+    if (input[(h - 1 - row) * w * 4 + col * 4 + boundary] != 0) {
+      if (
+        input[(h - 1 - (row + 1)) * w * 4 + col * 4 + boundary] != 0 &&
+        input[(h - 1 - row) * w * 4 + (col + 1) * 4 + interior] != 0
+      ) {
+        output[(h - 1 - row) * w * 4 + col * 4 + northsouth] |= north;
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= start;
+      }
+      if (input[(h - 1 - row) * w * 4 + (col - 1) * 4 + interior] != 0)
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= stop;
+    }
+    row = h - 1;
+    output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+    if (input[(h - 1 - row) * w * 4 + col * 4 + boundary] != 0) {
+      if (input[(h - 1 - row) * w * 4 + (col + 1) * 4 + interior] != 0)
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= stop;
+      if (
+        input[(h - 1 - (row - 1)) * w * 4 + col * 4 + boundary] != 0 &&
+        input[(h - 1 - row) * w * 4 + (col - 1) * 4 + interior] != 0
+      ) {
+        output[(h - 1 - row) * w * 4 + col * 4 + northsouth] |= south;
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= start;
+      }
+    }
+  }
+  for (row = 1; row < h - 1; ++row) {
+    col = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+    if (input[(h - 1 - row) * w * 4 + col * 4 + boundary] != 0) {
+      if (
+        input[(h - 1 - row) * w * 4 + (col + 1) * 4 + boundary] != 0 &&
+        input[(h - 1 - (row - 1)) * w * 4 + col * 4 + interior] != 0
+      ) {
+        output[(h - 1 - row) * w * 4 + col * 4 + eastwest] |= east;
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= start;
+      }
+      if (input[(h - 1 - (row + 1)) * w * 4 + col * 4 + interior] != 0)
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= stop;
+    }
+    col = w - 1;
+    output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+    output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+    if (input[(h - 1 - row) * w * 4 + col * 4 + boundary] != 0) {
+      if (input[(h - 1 - (row - 1)) * w * 4 + col * 4 + interior] != 0)
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= stop;
+      if (
+        input[(h - 1 - row) * w * 4 + (col - 1) * 4 + boundary] != 0 &&
+        input[(h - 1 - (row + 1)) * w * 4 + col * 4 + interior] != 0
+      ) {
+        output[(h - 1 - row) * w * 4 + col * 4 + eastwest] |= west;
+        output[(h - 1 - row) * w * 4 + col * 4 + startstop] |= start;
+      }
+    }
+  }
+  //
+  // orient corner states (todo)
+  //
+  row = 0;
+  col = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+  row = h - 1;
+  col = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+  row = 0;
+  col = w - 1;
+  output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+  row = h - 1;
+  col = w - 1;
+  output[(h - 1 - row) * w * 4 + col * 4 + northsouth] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + eastwest] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + startstop] = 0;
+  output[(h - 1 - row) * w * 4 + col * 4 + alpha] = 255;
+
+  // var display = new Uint8ClampedArray(h*w*4)
+  // var r,g,b,i
+  // for (row = 0; row < h; ++row) {
+  //   for (col = 0; col < w; ++col) {
+  //     r = output[(h-1-row)*w*4+col*4+0]
+  //     g = output[(h-1-row)*w*4+col*4+1]
+  //     b = output[(h-1-row)*w*4+col*4+2]
+  //     i = r+g+b
+  //     if (i != 0) {
+  //        display[(h-1-row)*w*4+col*4+0] = output[(h-1-row)*w*4+col*4+0]
+  //        display[(h-1-row)*w*4+col*4+1] = output[(h-1-row)*w*4+col*4+1]
+  //        display[(h-1-row)*w*4+col*4+2] = output[(h-1-row)*w*4+col*4+2]
+  //        display[(h-1-row)*w*4+col*4+3] = output[(h-1-row)*w*4+col*4+3]
+  //        }
+  //     else {
+  //        display[(h-1-row)*w*4+col*4+0] = 255
+  //        display[(h-1-row)*w*4+col*4+1] = 255
+  //        display[(h-1-row)*w*4+col*4+2] = 255
+  //        display[(h-1-row)*w*4+col*4+3] = 255
+  //        }
+  //     }
+  //  }
+
+  const imgData = new ImageData(output, w, h);
+
+  return imgData;
+};
+
+function vectorWorker() {
+  self.onmessage = function (e) {
+    const vectorizeHelper = (imageRGBA, vectorFit = 1, sort = true) => {
+      var h = imageRGBA.height;
+      var w = imageRGBA.width;
+      var input = imageRGBA.data;
+      var northsouth = 0;
+      var north = 128;
+      var south = 64;
+      var eastwest = 1;
+      var east = 128;
+      var west = 64;
+      var startstop = 2;
+      var start = 128;
+      var stop = 64;
+      var path = [];
+      //
+      // edge follower
+      //
+      function follow_edges(row, col) {
+        if (
+          input[(h - 1 - row) * w * 4 + col * 4 + northsouth] != 0 ||
+          input[(h - 1 - row) * w * 4 + col * 4 + eastwest] != 0
+        ) {
+          path[path.length] = [
+            [col, row]
+          ];
+          while (1) {
+            if (
+              input[(h - 1 - row) * w * 4 + col * 4 + northsouth] & north
+            ) {
+              input[(h - 1 - row) * w * 4 + col * 4 + northsouth] =
+                input[(h - 1 - row) * w * 4 + col * 4 + northsouth] &
+                ~north;
+              row += 1;
+              path[path.length - 1][path[path.length - 1].length] = [
+                col,
+                row
+              ];
+            } else if (
+              input[(h - 1 - row) * w * 4 + col * 4 + northsouth] & south
+            ) {
+              input[(h - 1 - row) * w * 4 + col * 4 + northsouth] =
+                input[(h - 1 - row) * w * 4 + col * 4 + northsouth] &
+                ~south;
+              row -= 1;
+              path[path.length - 1][path[path.length - 1].length] = [
+                col,
+                row
+              ];
+            } else if (
+              input[(h - 1 - row) * w * 4 + col * 4 + eastwest] & east
+            ) {
+              input[(h - 1 - row) * w * 4 + col * 4 + eastwest] =
+                input[(h - 1 - row) * w * 4 + col * 4 + eastwest] & ~east;
+              col += 1;
+              path[path.length - 1][path[path.length - 1].length] = [
+                col,
+                row
+              ];
+            } else if (
+              input[(h - 1 - row) * w * 4 + col * 4 + eastwest] & west
+            ) {
+              input[(h - 1 - row) * w * 4 + col * 4 + eastwest] =
+                input[(h - 1 - row) * w * 4 + col * 4 + eastwest] & ~west;
+              col -= 1;
+              path[path.length - 1][path[path.length - 1].length] = [
+                col,
+                row
+              ];
+            } else break;
+          }
+        }
+      }
+      //
+      // follow boundary starts
+      //
+      for (var row = 1; row < h - 1; ++row) {
+        col = 0;
+        follow_edges(row, col);
+        col = w - 1;
+        follow_edges(row, col);
+      }
+      for (var col = 1; col < w - 1; ++col) {
+        row = 0;
+        follow_edges(row, col);
+        row = h - 1;
+        follow_edges(row, col);
+      }
+      //
+      // follow interior paths
+      //
+      for (var row = 1; row < h - 1; ++row) {
+        for (var col = 1; col < w - 1; ++col) {
+          follow_edges(row, col);
+        }
+      }
+      //
+      // vectorize path
+      //
+      var error = vectorFit;
+      var vecpath = [];
+      for (var seg = 0; seg < path.length; ++seg) {
+        var x0 = path[seg][0][0];
+        var y0 = path[seg][0][1];
+        vecpath[vecpath.length] = [
+          [x0, y0]
+        ];
+        var xsum = x0;
+        var ysum = y0;
+        var sum = 1;
+        for (var pt = 1; pt < path[seg].length; ++pt) {
+          var xold = x;
+          var yold = y;
+          var x = path[seg][pt][0];
+          var y = path[seg][pt][1];
+          if (sum == 1) {
+            xsum += x;
+            ysum += y;
+            sum += 1;
+          } else {
+            var xmean = xsum / sum;
+            var ymean = ysum / sum;
+            var dx = xmean - x0;
+            var dy = ymean - y0;
+            var d = Math.sqrt(dx * dx + dy * dy);
+            var nx = dy / d;
+            var ny = -dx / d;
+            var l = Math.abs(nx * (x - x0) + ny * (y - y0));
+            if (l < error) {
+              xsum += x;
+              ysum += y;
+              sum += 1;
+            } else {
+              vecpath[vecpath.length - 1][
+                vecpath[vecpath.length - 1].length
+              ] = [xold, yold];
+              x0 = xold;
+              y0 = yold;
+              xsum = xold;
+              ysum = yold;
+              sum = 1;
+            }
+          }
+          if (pt == path[seg].length - 1) {
+            vecpath[vecpath.length - 1][
+              vecpath[vecpath.length - 1].length
+            ] = [x, y];
+          }
+        }
+      }
+      //
+      // sort path
+      //
+      if (vecpath.length > 1 && sort == true) {
+        var dmin = w * w + h * h;
+        var segmin = null;
+        for (var seg = 0; seg < vecpath.length; ++seg) {
+          var x = vecpath[seg][0][0];
+          var y = vecpath[seg][0][0];
+          var d = x * x + y * y;
+          if (d < dmin) {
+            dmin = d;
+            segmin = seg;
+          }
+        }
+        if (segmin != null) {
+          var sortpath = [vecpath[segmin]];
+          vecpath.splice(segmin, 1);
+        }
+        while (vecpath.length > 0) {
+          var dmin = w * w + h * h;
+          var x0 =
+            sortpath[sortpath.length - 1][
+            sortpath[sortpath.length - 1].length - 1
+            ][0];
+          var y0 =
+            sortpath[sortpath.length - 1][
+            sortpath[sortpath.length - 1].length - 1
+            ][1];
+          segmin = null;
+          for (var seg = 0; seg < vecpath.length; ++seg) {
+            var x = vecpath[seg][0][0];
+            var y = vecpath[seg][0][1];
+            var d = (x - x0) * (x - x0) + (y - y0) * (y - y0);
+            if (d < dmin) {
+              dmin = d;
+              segmin = seg;
+            }
+          }
+          if (segmin != null) {
+            sortpath[sortpath.length] = vecpath[segmin];
+            vecpath.splice(segmin, 1);
+          }
+        }
+      } else if (
+        (vecpath.length > 1 && sort == false) ||
+        vecpath.length == 1
+      )
+        sortpath = vecpath;
+      else sortpath = [];
+
+      return sortpath;
+    };
+
+    const newOut = vectorizeHelper(e.data);
+    self.postMessage(newOut);
+  };
+}
diff --git a/system/javascript/osapjs/client/components/imgCanvas.js b/system/javascript/osapjs/client/components/imgCanvas.js
new file mode 100644
index 0000000000000000000000000000000000000000..329a97d50609aac3a9b3e6f0cd519f418e83381d
--- /dev/null
+++ b/system/javascript/osapjs/client/components/imgCanvas.js
@@ -0,0 +1,59 @@
+/*
+domain.js
+
+click-to-go, other virtual machine dwg 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import dt from '../drawing/domtools.js'
+
+export default function ImgCanvas(xPlace, yPlace, width, height) {
+    // machine size : pixel size
+    let psize = [width, height]
+    let msize = [10, 10]
+    let scale = [msize[0] / psize[0], msize[1] / psize[1]]
+    // setup the pad
+    let dom = $('.plane').get(0)
+    let pad = $('<div>').addClass('pad').get(0)
+    $(pad).css('background-color', '#ffe').css('width', `${psize[0]}px`).css('height', `${psize[1]}px`)
+    $(dom).append(pad)
+    let dft = { s: 1, x: xPlace, y: yPlace, ox: 0, oy: 0 }
+    dt.writeTransform(pad, dft)
+    // canvas
+    let canvas = document.createElement('canvas')
+    $(pad).append(canvas)
+    console.log(pad)
+
+    this.draw = (imgdata) => {
+        console.log('drawing to canvas', imgdata)
+        // assert 100p alpha channel 
+        for (let i = 0; i < imgdata.data.length; i += 4) {
+            //imgdata.data[i] = 255;
+            imgdata.data[i + 3] = 255;
+        }
+        // these are weird,first we put imgdata into a 'virtual' canvas that we won't render 
+        let vcanvas = document.createElement('canvas')
+        vcanvas.width = imgdata.width
+        vcanvas.height = imgdata.height
+        vcanvas.getContext('2d').putImageData(imgdata, 0, 0)
+        // now we want to render at a fixed size: we pick our width and scale height to match 
+        let scale = width / imgdata.width
+        canvas.height = imgdata.height * scale
+        canvas.width = imgdata.width * scale // yes this should just re-assert that width = width 
+        $(pad).css('height', `${canvas.height}px`)
+        console.log(`scale to render by ${scale}`)
+        // now we scale the rendering context 
+        canvas.getContext('2d').scale(scale, scale)
+        // and draw into it, not imagdata, but the 'image' from the canvas... idk, 
+        canvas.getContext('2d').drawImage(vcanvas, 0, 0)
+    }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/imgOffset.js b/system/javascript/osapjs/client/components/imgOffset.js
new file mode 100644
index 0000000000000000000000000000000000000000..754074494179d01c5a0725c66120e7c2ae782d11
--- /dev/null
+++ b/system/javascript/osapjs/client/components/imgOffset.js
@@ -0,0 +1,26 @@
+export default function imgOffset(distances, offset, width, height){
+    var w = width;
+    var h = height;
+    var offset = offset;
+    var input = distances;
+    var output = new Uint8ClampedArray(4 * h * w);
+    for (var row = 0; row < h; ++row) {
+      for (var col = 0; col < w; ++col) {
+        if (input[(h - 1 - row) * w + col] <= offset) {
+          output[(h - 1 - row) * w * 4 + col * 4 + 0] = 255;
+          output[(h - 1 - row) * w * 4 + col * 4 + 1] = 255;
+          output[(h - 1 - row) * w * 4 + col * 4 + 2] = 255;
+          output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+        } else {
+          output[(h - 1 - row) * w * 4 + col * 4 + 0] = 0;
+          output[(h - 1 - row) * w * 4 + col * 4 + 1] = 0;
+          output[(h - 1 - row) * w * 4 + col * 4 + 2] = 0;
+          output[(h - 1 - row) * w * 4 + col * 4 + 3] = 255;
+        }
+      }
+    }
+
+    const imgData = new ImageData(output, w, h);
+
+    return imgData;
+}
diff --git a/system/javascript/osapjs/client/components/jogBox.js b/system/javascript/osapjs/client/components/jogBox.js
new file mode 100644
index 0000000000000000000000000000000000000000..3adc42ffd8cf482dabc2f523a368779612499500
--- /dev/null
+++ b/system/javascript/osapjs/client/components/jogBox.js
@@ -0,0 +1,273 @@
+/*
+jogBox.js
+
+jog input 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import { Button, TextInput, TextBlock } from '../interface/basics.js'
+
+let BTN_ERRTIME = 2000
+
+function JogBox(xPlace, yPlace, vm, rate) {
+  // jog 
+  let jogBtn = Button(xPlace, yPlace, 84, 94, 'click-in to jog')
+  let jogBigInput = TextInput(xPlace, yPlace + 110, 87, 20, '100.0')
+  let jogNormalInput = TextInput(xPlace, yPlace + 140, 87, 20, '1.0')
+  let jogSmallInput = TextInput(xPlace, yPlace + 170, 87, 20, '0.1')
+  let status = TextBlock(xPlace, yPlace + 200, 84, 14, '...')
+  // key status 
+  let eDown = false;
+  let setE = (bool) => {
+    eDown = bool
+    if (eDown) {
+      status.setText('e')
+    } else {
+      status.setText('xy')
+    }
+  }
+  let zDown = false;
+  let setZ = (bool) => {
+    zDown = bool
+    if (zDown) {
+      status.setText('z')
+    } else {
+      status.setText('xy')
+      // do... 
+    }
+  }
+  let bigDown = false;
+  let setBig = (bool) => {
+    bigDown = bool
+    if (bigDown) {
+      setSmall(false)
+      setNormal(false)
+      jogBigInput.green()
+    } else {
+      jogBigInput.grey()
+    }
+  }
+  let normalDown = false;
+  let setNormal = (bool) => {
+    normalDown = bool
+    if (normalDown) {
+      setSmall(false)
+      setBig(false)
+      jogNormalInput.green()
+    } else {
+      jogNormalInput.grey()
+    }
+  }
+  let smallDown = false;
+  let setSmall = (bool) => {
+    smallDown = bool
+    if (smallDown) {
+      setNormal(false)
+      setBig(false)
+      jogSmallInput.green()
+    } else {
+      jogSmallInput.grey()
+    }
+  }
+  // clear 
+  let noneDown = () => {
+    setNormal(false)
+    setBig(false)
+    setSmall(false)
+  }
+  // action
+  let getIncrement = () => {
+    let val = 0
+    // console.log(smallDown, normalDown, bigDown)
+    if (smallDown) {
+      return jogSmallInput.getNumber()
+    } else if (normalDown) {
+      return jogNormalInput.getNumber()
+    } else if (bigDown) {
+      return jogBigInput.getNumber()
+    } else {
+      console.error('no increment selected, statemachine borked')
+      return 0
+    }
+  }
+
+  let jogging = false
+  let jogLog = false 
+
+  let jog = (key, rate) => {
+    jogging = true
+    jogBtn.yellow('...')
+    if(jogLog) console.log('jog: await no motion')
+    vm.motion.awaitMotionEnd().then(() => {
+      if(jogLog) console.log('jog: set wait time')
+      return vm.motion.setWaitTime(10)
+    }).then(() => {
+      if(jogLog) console.log('jog: get pos')
+      return vm.motion.getPos()
+    }).then((pos) => {
+      // aaaah, hotfix for extruder moves, 
+      pos.E = 0
+      let inc = getIncrement()
+      switch (key) {
+        case 'left':
+          pos.X -= inc
+          return vm.motion.addMoveToQueue({ position: pos, rate: rate })
+        case 'right':
+          pos.X += inc
+          return vm.motion.addMoveToQueue({ position: pos, rate: rate })
+        case 'up':
+          if (zDown) {
+            pos.Z += inc
+          } else if (eDown) {
+            pos.E -= inc
+            // same note as below, this is hack, 
+            return vm.motion.addMoveToQueue({ position: pos, rate: 4 })
+          } else {
+            pos.Y += inc
+          }
+          return vm.motion.addMoveToQueue({ position: pos, rate: rate })
+        case 'down':
+          if (zDown) {
+            pos.Z -= inc
+          } else if (eDown) {
+            pos.E += inc
+            // bit hack, rate is global when passed in during construct, shouldn't be 
+            return vm.motion.addMoveToQueue({ position: pos, rate: 2 })
+          } else {
+            pos.Y -= inc
+          }
+          return vm.motion.addMoveToQueue({ position: pos, rate: rate })
+        default:
+          console.error('bad key for jog switch')
+          break;
+      }
+    }).then(() => {
+      if(jogLog) console.log('jog: await no motion')
+      return vm.motion.awaitMotionEnd()
+    }).then(() => {
+      if(jogLog) console.log('jog: set wait time')
+      return vm.motion.setWaitTime(1000)
+    }).then(() => {
+      if(jogLog) console.log('jog: restart jog')
+      jogging = false
+      this.restart()
+    }).catch((err) => {
+      console.error(err)
+      jogBtn.red('jog error, see console')
+      setTimeout(() => {
+        jogBtn.clicked = false
+        this.restart()
+      }, BTN_ERRTIME)
+    })
+  }
+  // key listeners 
+  this.keyDownListener = (evt) => {
+    if (jogging) return
+    if (evt.repeat) return
+    switch (evt.keyCode) {
+      case 69:
+        setE(true)
+        break;
+      case 90:
+        setZ(true)
+        break;
+      case 88:
+        setBig(true)
+        break;
+      case 67:
+        setSmall(true)
+        break;
+      case 38:
+        jog('up', rate)   
+        break;
+      case 40:
+        jog('down', rate)
+        break;
+      case 37:
+        jog('left', rate)
+        break;
+      case 39:
+        jog('right', rate)
+        break;
+      default:
+        break;
+    }
+  }
+  // up 
+  this.keyUpListener = (evt) => {
+    //console.log('keyup', evt.keyCode)
+    switch (evt.keyCode) {
+      case 69:
+        setE(false);
+      case 90:
+        setZ(false);
+      case 88:
+        setBig(false)
+        setNormal(true)
+        break;
+      case 67:
+        setSmall(false)
+        setNormal(true)
+        break;
+      default:
+        break;
+    }
+  }
+  // in-to-state
+  this.start = () => {
+    jogBtn.clicked = true
+    jogBtn.green()
+    jogBtn.setHTML('x: big inc<br>c: small inc<br>z: map y->z<br>e: map y->e')
+    status.setText('xy')
+    document.addEventListener('keydown', this.keyDownListener)
+    document.addEventListener('keyup', this.keyUpListener)
+    if (bigDown) {
+      setBig(true)
+    } else if (smallDown) {
+      setSmall(true)
+    } else {
+      setNormal(true)
+    }
+    if (zDown) {
+      status.setText('z')
+    }
+  }
+  // out-of 
+  this.stop = () => {
+    jogBtn.clicked = false
+    jogBtn.grey('click in to jog')
+    status.grey('...')
+    noneDown()
+    document.removeEventListener('keydown', this.keyDownListener)
+    document.removeEventListener('keyup', this.keyUpListener)
+  }
+  // restart w/ varied button-down state 
+  this.restart = () => {
+    if (jogBtn.clicked) {
+      this.start()
+    } else {
+      this.stop()
+    }
+  }
+  // go big 
+  this.select
+  // ok, statemachine 
+  jogBtn.onClick(() => {
+    if (!jogBtn.clicked) {
+      this.start()
+    } else {
+      this.stop()
+    }
+  })
+}
+
+export { JogBox }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/loadPanel.js b/system/javascript/osapjs/client/components/loadPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce3a3ad6b91103597246e61ea3d634a45c77c5ce
--- /dev/null
+++ b/system/javascript/osapjs/client/components/loadPanel.js
@@ -0,0 +1,51 @@
+/*
+loadPanel.js
+
+loadcell amp UI
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import { Button } from '../interface/basics.js'
+import AutoPlot from '../../client/components/autoPlot.js'
+
+export default function LoadPanel(vm, xPlace, yPlace, name) {
+  let title = new Button(xPlace, yPlace, 104, 34, name)
+  let loadPlot = new AutoPlot(xPlace + 120, yPlace, 700, 400)
+  loadPlot.setHoldCount(1000)
+  loadPlot.redraw()
+
+  let lpBtn = new Button(xPlace, yPlace + 50, 104, 14, 'plot load')
+  let lp = false
+  let lpCount = 0
+  lpBtn.onClick(() => {
+    if (lp) {
+      lp = false
+      lpBtn.good('stopped', 500)
+    } else {
+      let poll = () => {
+        if(!lp) return;
+        vm.getReading().then((reading) => {
+          lpCount ++ 
+          loadPlot.pushPt([lpCount, reading[0]])
+          loadPlot.redraw() 
+          setTimeout(poll, 100)
+        }).catch((err) => {
+          lp = false 
+          console.error(err)
+          lpBtn.bad("err", 500)
+        })
+      }
+      lp = true 
+      setTimeout(poll, 250)
+    }
+  })
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/machineBed.js b/system/javascript/osapjs/client/components/machineBed.js
new file mode 100644
index 0000000000000000000000000000000000000000..d674b36fef96d263c9321b82c8bbb2fec5673b24
--- /dev/null
+++ b/system/javascript/osapjs/client/components/machineBed.js
@@ -0,0 +1,446 @@
+/*
+machineBed.js
+
+js-dom rep of a machine bed, for 2D-ish CNC... svg, images, etc? 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import TIME from '../../core/time.js'
+
+import { Button, TextBlock } from '../interface/basics.js'
+import dt from '../interface/domTools.js'
+import gerberConverter from './gerberConverter.js'
+import ImgToPath2D from './img2path.js'
+
+// this is apparently... a constant? in graphics? tf?
+let mmToPixel = 3 / 0.79375
+let pixelToMM = 1 / mmToPixel
+
+export default function MachineBed(settings, machine) {
+  // -------------------------------------------- Build the Pad... 
+  // stash machine, render sizes, 
+  let mDims = [machine.settings.bounds[0], machine.settings.bounds[1]]
+  let rDims = [settings.renderWidth, mDims[1] / mDims[0] * settings.renderWidth]
+  this.getRenderDims = () => {
+    return JSON.parse(JSON.stringify(rDims))
+  }
+  let machineToRenderScale = rDims[0] / mDims[0]
+  let renderToMachineScale = mDims[0] / rDims[0]
+  // trash warning
+  console.warn(`pixelToMM`, pixelToMM)
+  console.warn(`mmToPixel`, mmToPixel)
+  console.warn(`machineToRender ${mDims[0]} -> ${rDims[0]}`, machineToRenderScale)
+  console.warn(`renderToMachine ${rDims[0]} -> ${mDims[0]}`, renderToMachineScale)
+  // get the plane, add the pad, 
+  let dom = $('.plane').get(0)
+  this.elem = $('<div>')
+    .css('position', 'absolute')
+    .css('width', `${rDims[0]}px`)
+    .css('height', `${rDims[1]}px`)
+    .css('background-color', '#fff')
+    .css('left', `${settings.xPlace}px`)
+    .css('top', `${settings.yPlace}px`)
+    .css('border', `1px solid rgb(225, 225, 225)`)
+    .get(0)
+  $(dom).append(this.elem)
+
+  let colX = settings.xPlace
+  let colY = settings.yPlace + Math.ceil(rDims[1] / 10) * 10 + 10
+
+  // build a reporting block, 
+  let messageBox = new TextBlock({
+    xPlace: colX,
+    yPlace: colY,
+    width: settings.renderWidth,
+    height: 30,
+    defaultText: `...`
+  }, true)
+
+  // -------------------------------------------- Drag 'n Drop 
+
+  $(this.elem).on('dragover', (evt) => {
+    // console.log('dragover', evt)
+    evt.preventDefault()
+  })
+
+  $(this.elem).on('drop', (evt) => {
+    // walk over jquery's bottle 
+    evt = evt.originalEvent
+    evt.preventDefault()
+    // rm our old layers... eventually we could check-guard against rm'ing stateful things here, 
+    // for (let layer of layers) {
+    //   layer.btn.remove()
+    //   layer.remove()
+    // }
+    // get for-items... 
+    if (evt.dataTransfer.items) {
+      [...evt.dataTransfer.items].forEach((item, i) => {
+        if (item.kind === 'file') {
+          // ~ to use the File API 
+          let file = item.getAsFile()
+          console.log(`… file[${i}].name = ${file.name}`)
+          // filter for names... 
+          let layerName = ''
+          if (file.name.includes('trace') || file.name.includes('copper_top')) {
+            layerName = 'topTraces'
+          } else if (file.name.includes('interior') || file.name.includes('profile')) {
+            layerName = 'outline'
+          } else {
+            console.error(`no known layer type for ${file.name}, bailing...`)
+          }
+          // ok, now switch-import on types, 
+          if (file.name.includes('.gbr')) {
+            throw new Error('need to rework')
+            // this should convert the layer to PNG, 
+          } else if (file.name.includes('.png')) {
+            // use a fileReaderto get the data, 
+            // this could be a ute... 
+            let reader = new FileReader()
+            reader.addEventListener('load', () => {
+              // console.log(reader.result)
+              // we want to collect an ImageData from this thing, 
+              let image = new Image()
+              image.onload = () => {
+                console.log(image)
+                let canvas = document.createElement('canvas')
+                canvas.width = image.width
+                canvas.height = image.height
+                let context = canvas.getContext('2d')
+                context.drawImage(image, 0, 0, image.width, image.height)
+                let imageData = context.getImageData(0, 0, image.width, image.height)
+                console.log(imageData)
+                this.addLayer({
+                  name: layerName,
+                  imageData: imageData,
+                  dpi: 1000,
+                })
+              } // end image onload 
+              image.onerror = (err) => {
+                console.error(err)
+              }
+              image.src = reader.result
+            })
+            reader.addEventListener('error', (err) => {
+              console.error(err)
+            })
+            reader.readAsDataURL(file)
+          } else { // end .png case 
+            console.error(`unknown file type encountered ${file.name}`)
+          }
+        }
+      })
+    } else {
+      console.error(`jake hasn't handled this case...`)
+      // see https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop 
+    }
+  })
+
+  // -------------------------------------------- Ingest Layers 
+  // we keep a stack of layers... in a job object, 
+  let job = {
+    position: [0, 0],
+    layers: {},
+    elem: $('<div>')
+      .attr('id', 'jobtainer')
+      .css('position', 'absolute')  // it's abs-position 
+      .css('transform-origin', 'top left')  // scale from top left,
+      .css('left', `0px`)
+      .css('top', `0px`)
+      .get(0)
+  }
+
+  $(this.elem).append(job.elem)
+  /* layers are like... 
+  { 
+    name: <layerName: top, bottom, or outline>, 
+    imageData: <ImageData>,   // this has .width and .height, then with DPI we have size 
+    dpi: <num = 1000> 
+  }
+  */
+  this.addLayer = async (layer) => {
+    try {
+      console.warn(layer)
+      // we'll want a scale which is relative our rendering size... 
+      // imageData is pixels-across, 
+      let realX = layer.imageData.width / layer.dpi * 25.4
+      console.log(`real width is ${realX}, machine width ${mDims[0]}`)
+      let renderX = (realX / mDims[0]) * rDims[0]
+      console.log(`render width should be ${renderX}`)
+      let scale = renderX / layer.imageData.width
+      // and we have a machine
+      console.log(`scale at ${scale} would set width to ${layer.imageData.width * scale}`)
+      // we make a canvas that's appropriately sized for our "machine bed"
+      // ... we'll use the original ImageData for paths anyways, this is messed up, we make a 
+      // virtual canvas to fit the OG perfectly:
+      let virtualCanvas = document.createElement('canvas')
+      virtualCanvas.width = layer.imageData.width
+      virtualCanvas.height = layer.imageData.height
+      // we load our imageData there, 
+      virtualCanvas.getContext('2d').putImageData(layer.imageData, 0, 0)
+      // now we make another, which is scaled as we'd like, this what we'll actually render, 
+      let canvas = document.createElement('canvas')
+      $(canvas).css('position', 'absolute')
+      canvas.width = layer.imageData.width * scale
+      canvas.height = layer.imageData.height * scale
+      // now we draw from one to the other, 
+      let context = canvas.getContext('2d')
+      // top layer should render at 50% opacity, 
+      if (layer.name == 'topTraces') context.globalAlpha = 0.25
+      context.drawImage(virtualCanvas, 0, 0, canvas.width, canvas.height)
+      // append that... 
+      layer.elem = canvas
+      $(job.elem).append(layer.elem)
+      job.layers[layer.name] = layer
+      // if that was the bottom layer and the top already exists...
+      if (layer.name == 'outline' && job.layers.topTraces) {
+        console.warn(`rearranging layers... sending outline to back`)
+        // rm both... 
+        $(job.layers.outline.elem).remove()
+        $(job.layers.topTraces.elem).remove()
+        // add back in,
+        $(job.elem).append(job.layers.outline.elem)
+        $(job.elem).append(job.layers.topTraces.elem)
+      }
+    } catch (err) {
+      console.error(err)
+    }
+  }
+
+  // let's move that around on mouse drags,
+  this.elem.addEventListener('mousedown', (evt) => {
+    evt.preventDefault()
+    evt.stopPropagation()
+    this.elem.removeEventListener('mousemove', reportMousePosn)
+    dt.dragTool((drag) => {
+      drag.preventDefault()
+      drag.stopPropagation()
+      let ct = dt.readTransform(job.elem)
+      ct.x += drag.movementX
+      ct.y += drag.movementY
+      dt.writeTransform(job.elem, ct)
+    }, (up) => {
+      // add this back in, 
+      this.elem.addEventListener('mousemove', reportMousePosn)
+      if (!job.layers.outline || !job.layers.topTraces) return
+      // update the job position, given new pad position... 
+      let ct = dt.readTransform(job.elem)
+      let mx = ct.x * renderToMachineScale
+      let my = (rDims[1] - ct.y - job.layers.outline.elem.height) * renderToMachineScale
+      console.log(`mx, my,`, mx, my)
+      job.position[0] = mx
+      job.position[1] = my
+      if (machine.available) {
+        machine.getPosition().then((pos) => {
+          return machine.gotoPosition([mx, my, pos[2]])
+        }).then(() => {
+          console.log(`on job drag, moved machine to ${mx.toFixed(2)}, ${my.toFixed(2)}`)
+          // ok, 
+        }).catch((err) => {
+          console.error(err)
+        })
+      }
+    })
+  })
+
+  let reportMousePosn = (evt) => {
+    if (evt.target != this.elem) return
+    messageBox.setText(`mx: ${(evt.layerX * renderToMachineScale).toFixed(2)}\tmy: ${((rDims[1] - evt.layerY) * renderToMachineScale).toFixed(2)}`)// px: ${evt.layerX}\tpy: ${evt.layerY}`)
+  }
+
+  this.elem.addEventListener('mousemove', reportMousePosn)
+
+  // -------------------------------------------- Bed Click Events
+
+  this.elem.addEventListener('click', async (evt) => {
+    if (evt.target != this.elem) return
+    // console.log('xy...', evt.layerX * renderToMachineScale, evt.layerY * renderToMachineScale)
+    try {
+      if (machine.available) {
+        let mx = evt.layerX * renderToMachineScale
+        let my = (rDims[1] - evt.layerY) * renderToMachineScale
+        console.warn(`GOTO ${mx}, ${my} ...`)
+        let pos = await machine.getPosition()
+        await machine.gotoPosition([mx, my, pos[2]])
+        pos = await machine.getPosition()
+        console.log(`went to ${mx.toFixed(2)}, ${my.toFixed(2)}`)
+        console.log(`retrieved ${pos[0].toFixed(2)}, ${pos[1].toFixed(2)}`)
+      }
+    } catch (err) {
+      console.error(err)
+    }
+  })
+
+  // -------------------------------------------- Genny & Mill Buttons 
+
+  let genTracesBtn = new Button({
+    xPlace: colX,
+    yPlace: colY += 50,
+    width: 200,
+    height: 30,
+    defaultText: `generate traces plan`
+  })
+
+  let runTracesBtn = new Button({
+    xPlace: colX + 210,
+    yPlace: colY,
+    width: 200,
+    height: 30,
+    defaultText: 'run traces plan'
+  })
+
+  let genOutlineBtn = new Button({
+    xPlace: colX,
+    yPlace: colY += 40,
+    width: 200,
+    height: 30,
+    defaultText: `generate outline plan`
+  })
+
+  let runOutlineBtn = new Button({
+    xPlace: colX + 210,
+    yPlace: colY,
+    width: 200,
+    height: 30,
+    defaultText: 'run outline plan'
+  })
+
+  // let's add the button fns, 
+  genTracesBtn.onClick(async () => {
+    try {
+      if (!job.layers.outline) {
+        genTracesBtn.yellow(`please add files first...`)
+        setTimeout(() => {
+          genTracesBtn.resetText()
+          genTracesBtn.grey()
+        }, 1000)
+        return 
+      }
+      genTracesBtn.yellow(`calculating path...`)
+      let path = await ImgToPath2D({
+        imageData: job.layers.topTraces.imageData,
+        realWidth: job.layers.topTraces.imageData.width / job.layers.topTraces.dpi * 25.4,
+        toolOffset: (1 / 64) * 0.5 * 25.4,
+        zUp: 2,
+        zDown: -0.15,
+        passDepth: 0.15,
+        feedRate: 10,
+        jogRate: 100,
+      })
+      genTracesBtn.green(`traces gennie'd`)
+      job.layers.topTraces.path = path
+    } catch (err) {
+      genTracesBtn.red(`error, see console...`)
+      console.error(err)
+    }
+  })
+
+  // let's add the button fns, 
+  genOutlineBtn.onClick(async () => {
+    try {
+      if (!job.layers.outline) {
+        genOutlineBtn.yellow(`please add files first...`)
+        setTimeout(() => {
+          genOutlineBtn.resetText()
+          genOutlineBtn.grey()
+        }, 1000)
+        return 
+      }
+      genOutlineBtn.yellow(`calculating path...`)
+      let path = await ImgToPath2D({
+        imageData: job.layers.outline.imageData,
+        realWidth: job.layers.outline.imageData.width / job.layers.outline.dpi * 25.4,
+        toolOffset: (1 / 32) * 0.5 * 25.4,  // in mm, 
+        zUp: 2,
+        zDown: -1.7,
+        passDepth: 0.3,
+        feedRate: 8,
+        jogRate: 100,
+      })
+      genOutlineBtn.green(`outline gennie'd`)
+      job.layers.outline.path = path
+      for(let move of path){
+        console.log(move.target)
+      }
+    } catch (err) {
+      genOutlineBtn.red(`error, see console...`)
+      console.error(err)
+    }
+  })
+
+  runTracesBtn.onClick(async () => {
+    try {
+      if (job.layers.topTraces && job.layers.topTraces.path) {
+        // offset 'em 
+        for (let move of job.layers.topTraces.path) {
+          move.target[0] += job.position[0]
+          move.target[1] += job.position[1]
+        }  
+        // spindle on, and wait for spool 
+        await machine.spindleVM.setDuty(0.30)
+        await TIME.delay(500)
+        // send each... 
+        for (let p in job.layers.topTraces.path) {
+          runTracesBtn.yellow(`sending ${p} / ${job.layers.topTraces.path.length - 1}`)
+          await machine.addMoveToQueue(job.layers.topTraces.path[p])
+        }
+        await machine.awaitMotionEnd()
+        await machine.spindleVM.setDuty(0)
+        await machine.park()
+        runTracesBtn.green(`done`)
+      } else {
+        runTracesBtn.yellow(`please gen plan first... `)
+        setTimeout(() => {
+          runTracesBtn.resetText()
+          runTracesBtn.grey()
+        }, 1000)
+      }
+    } catch (err) {
+      runTracesBtn.red(`error, see console...`)
+      console.error(err)
+    }
+  })
+
+  runOutlineBtn.onClick(async () => {
+    try {
+      if (job.layers.outline && job.layers.outline.path) {
+        // offset 'em 
+        for (let move of job.layers.outline.path) {
+          move.target[0] += job.position[0]
+          move.target[1] += job.position[1]
+        }
+        // spindle on, and wait for spool 
+        await machine.spindleVM.setDuty(0.30)
+        await TIME.delay(500)       
+        // run 'em 
+        for (let p in job.layers.outline.path) {
+          runOutlineBtn.yellow(`sending ${p} / ${job.layers.outline.path.length - 1}`)
+          console.log(`send move... ${job.layers.outline.path[p].target[0].toFixed(2)}, ${job.layers.outline.path[p].target[1].toFixed(2)}`)
+          await machine.addMoveToQueue(job.layers.outline.path[p])
+        }
+        await machine.awaitMotionEnd()
+        await machine.spindleVM.setDuty(0)
+        await machine.park()
+        runOutlineBtn.green(`done`)
+      } else {
+        runOutlineBtn.yellow(`please gen plan first... `)
+        setTimeout(() => {
+          runOutlineBtn.resetText()
+          runOutlineBtn.grey()
+        }, 1000)
+      }
+    } catch (err) {
+      runOutlineBtn.red(`error, see console...`)
+      console.error(err)
+    }
+  })
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/pad.js b/system/javascript/osapjs/client/components/pad.js
new file mode 100644
index 0000000000000000000000000000000000000000..f137ba6614a9eba8b556cf51d9b261b736da85c5
--- /dev/null
+++ b/system/javascript/osapjs/client/components/pad.js
@@ -0,0 +1,97 @@
+/*
+domain.js
+
+click-to-go, other virtual machine dwg 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import dt from '../interface/domTools.js'
+
+export default function Pad(xPlace, yPlace, width, height, machineX = 100, machineY = 100) {
+  // machine size : pixel size
+  let psize = [width, height]
+  let msize = [machineX, machineY]
+  let scale = [msize[0] / psize[0], msize[1] / psize[1]]
+  // setup the pad
+  let dom = $('.plane').get(0)
+  let pad = $('<div>').addClass('pad').get(0)
+  $(pad).css('background-color', '#c9e5f2').css('width', `${psize[0]}px`).css('height', `${psize[1]}px`)
+  $(dom).append(pad)
+  let dft = { s: 1, x: xPlace, y: yPlace, ox: 0, oy: 0 }
+  dt.writeTransform(pad, dft)
+  // drawing lines, 
+  let pts = [] // [[x,y],[]]
+  let pos = [0,0]
+  this.redraw = () => {
+    $(pad).children('.svgcont').remove() // rm all segs 
+    for (let p = 1; p < pts.length; p++) {
+      let del = [pts[p - 1][0] - pts[p][0], pts[p - 1][1] - pts[p][1]]
+      $(pad).append(dt.svgLine(
+        pts[p][0] / scale[0], psize[1] - pts[p][1] / scale[1],
+        del[0] / scale[0], - del[1] / scale[1]
+      ))
+    }
+    this.drawPosition(pos)
+  }
+
+  this.onNewTarget = (pos) => {
+    console.warn('bind this')
+  }
+
+  // also bind-able 
+  this.onDragTarget = (pos) => {}
+  this.onUp = (pos) => {}
+
+  this.addPoint = (pt) => {
+    //console.log('draw', pt)
+    pts.push(pt)
+    if (pts.length > 2500) pts.shift()
+    //drawPts()
+  }
+
+  this.drawPosition = (pt) => {
+    pos = [pt[0],pt[1]]
+    $(pad).children('#ptagID').remove()
+    $(pad).append(dt.svgLine(
+      pos[0] / scale[0], psize[1] - pos[1] / scale[1],
+      10, 10,
+      1, 'ptagID'
+    ))
+  }
+
+  // handle clicks 
+  pad.addEventListener('mousedown', (evt) => {
+    if (evt.target != pad) return
+    // scaled to machine spec, and invert y pixels -? logical 
+    let pos = [evt.layerX * scale[0], machineY - evt.layerY * scale[1]]
+    //console.warn(`X: ${pos[0].toFixed(2)}, Y: ${pos[1].toFixed(2)}`)
+    this.onNewTarget(pos)
+    // also do
+
+    document.addEventListener('mousemove', moveListener)
+    document.addEventListener('mouseup', upListener)
+  })
+
+  let moveListener = (evt) => {
+    if(evt.target != pad) return
+    let pos = [evt.layerX * scale[0], machineY - evt.layerY * scale[1]]
+    this.onDragTarget(pos)
+  }
+
+  let upListener = (evt) => {
+    if(evt.target != pad) return
+    let pos = [evt.layerX * scale[0], machineY - evt.layerY * scale[1]]
+    this.onUp(pos)
+    document.removeEventListener('mousemove', moveListener)
+    document.removeEventListener('mouseup', upListener)
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/simpleJog.js b/system/javascript/osapjs/client/components/simpleJog.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9fa8667083ea61cac214d7b1ab9bb49e9471d5b
--- /dev/null
+++ b/system/javascript/osapjs/client/components/simpleJog.js
@@ -0,0 +1,188 @@
+/*
+simpleJog.js
+
+better jog, for use w/ accel-integrating... 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import { EZButton } from '../interface/button.js'
+
+export default function SimpleJog(xPlace, yPlace, vm, rate){
+
+    // rate
+
+    let jogAccel = rate
+
+    // ------------------------------------------ buttons 
+
+    let leftBtn = EZButton(xPlace, yPlace, 14, 84, '<')
+    let midBtn = EZButton(xPlace + 30, yPlace, 84, 84, 'click-in to jog')
+    let rightBtn = EZButton(xPlace + 130, yPlace, 14, 84, '>')
+
+    // ------------------------------------------ buf of moves
+    // on down-promise return: is key still down? if not, release accel 
+    // if any key during promise, nothing happens 
+    // or before that, have pos-query just query accel, see what's up? 
+
+    let buf = [] 
+    let bufRunning = false 
+    
+    let bufAdd = (move) => {
+        buf.push(move)
+        //console.log('queue len', buf.length)
+        if(buf.length == 1){
+            bufRun()
+        }
+    }
+
+    let bufMoveComplete = () => {
+        bufRunning = false 
+        bufRun()
+    }
+    
+    let bufRun = () => {
+        if(buf.length == 0 || bufRunning) return;
+        let move = buf.shift()
+        bufRunning = true 
+        switch(move){
+            case "left_down":
+                vm.setAccel(-jogAccel).then(() => {
+                    bufMoveComplete()
+                }).catch(bufError)
+                break;
+            case "left_up":
+                vm.setAccel(0).then(() => {
+                    bufMoveComplete()
+                }).catch(bufError)
+                break;
+            case "right_down":
+                vm.setAccel(jogAccel).then(() => {
+                    bufMoveComplete()
+                }).catch(bufError)
+                break;
+            case "right_up":
+                vm.setAccel(0).then(() => {
+                    bufMoveComplete()
+                }).catch(bufError)
+                break;
+            default:
+                throw new Error("unrecognized move in jog buf")
+        }
+    }
+    
+    let bufError = (err) => {
+        console.error('during jog, error below:')
+        console.error(err)
+        buf.length = 0 
+    }
+
+    // ------------------------------------------ button clicks 
+
+    leftBtn.onClick(() => {
+        down("left")
+        setTimeout(() => {
+            up("left")
+        }, 200)
+    })
+
+    rightBtn.onClick(() => {
+        down("right")
+        setTimeout(() => {
+            up("right")
+        }, 200)
+    })
+
+    // ------------------------------------------ actions... should buf ? 
+
+    let down = (dir) => {
+        switch(dir){
+            case "left":
+                leftBtn.green()
+                bufAdd('left_down')
+                break;
+            case "right":
+                rightBtn.green()
+                bufAdd('right_down')
+                break;
+            default:
+                break;
+        }
+    }
+
+    let up = (dir) => {
+        switch(dir){
+            case "left":
+                leftBtn.grey()
+                bufAdd('left_up')
+                break;
+            case "right":
+                rightBtn.grey()
+                bufAdd('right_up')
+                break;
+            default:
+                break;
+        }
+    }
+
+    // ------------------------------------------ keydown states 
+
+    let keyStatus = false
+    midBtn.onClick(() => {
+        if(keyStatus){
+            keyStatus = false
+            removeKeys()
+        } else {
+            keyStatus = true
+            setupKeys()
+        }
+    })
+
+    let keyDown = (evt) => {
+        if(evt.repeat) return;
+        switch(evt.keyCode){
+            case 37:
+                down('left')
+                break;
+            case 39:
+                down('right')
+                break;
+            default:
+                break;
+        }
+    }
+
+    let keyUp = (evt) => {
+        switch(evt.keyCode){
+            case 37:
+                up('left')
+                break;
+            case 39:
+                up('right')
+                break;
+            default:
+                break;
+        }
+    }
+
+    let setupKeys = () => {
+        midBtn.green() 
+        document.addEventListener('keydown', keyDown)
+        document.addEventListener('keyup', keyUp)
+    }
+
+    let removeKeys = () => {
+        midBtn.grey()
+        document.removeEventListener('keydown', keyDown)
+        document.removeEventListener('keyup', keyUp)
+    }
+
+}
diff --git a/system/javascript/osapjs/client/components/tempPanel.js b/system/javascript/osapjs/client/components/tempPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e685bf89f8a699b02e7e521218f929231ea43ae
--- /dev/null
+++ b/system/javascript/osapjs/client/components/tempPanel.js
@@ -0,0 +1,148 @@
+/*
+tempPanel.js
+
+temperature / 'heater module' circuit UI 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from '../interface/domTools.js'
+import { Button, EZButton, TextInput, TextBlock } from '../interface/basics.js'
+import AutoPlot from '../../client/components/autoPlot.js'
+
+export default function TempPanel(vm, xPlace, yPlace, init, name, pidDisplay = false, pcfPresent = false) {
+  let title = new TextBlock(xPlace, yPlace, 84, 34, name)
+
+  yPlace += 50
+  let tempSet = new TextInput(xPlace, yPlace, 87, 20, `${init}`)
+
+  let tempSetBtn = new EZButton(xPlace, yPlace + 30, 84, 14, 'set temp')
+  tempSetBtn.onClick(() => {
+    let temp = parseFloat(tempSet.value)
+    if (Number.isNaN(temp)) {
+      tempSetBtn.bad("parse err", 1000)
+      return
+    }
+    vm.setExtruderTemp(temp).then(() => {
+      tempSetBtn.good("ok", 500)
+    }).catch((err) => {
+      console.error(err)
+      tempSetBtn.bad("err", 1000)
+    })
+  })
+
+  let tempCoolBtn = new EZButton(xPlace, yPlace + 60, 84, 14, 'cooldown')
+  tempCoolBtn.onClick(() => {
+    vm.setExtruderTemp(0).then(() => {
+      tempCoolBtn.good("ok", 500)
+    }).catch((err) => {
+      console.error(err)
+      tempCoolBtn.bad("err", 500)
+    })
+  })
+
+  let tempPlot = new AutoPlot(xPlace + 100, yPlace - 50, 400, 230,
+    `${name} temp`, { top: 40, right: 20, bottom: 30, left: 40 })
+  tempPlot.setHoldCount(1000)
+  tempPlot.setYDomain(0, init + 20)
+  tempPlot.redraw()
+
+  let effortPlot = {}
+
+  if (pidDisplay) {
+    effortPlot = new AutoPlot(xPlace + 100, yPlace + 240 - 50, 300, 150,
+      `${name} heater effort`, { top: 40, right: 20, bottom: 30, left: 40 })
+    effortPlot.setHoldCount(500)
+    //effortPlot.setYDomain(-10, 10)
+    effortPlot.redraw()
+  }
+
+  let tempLpBtn = new Button(xPlace, yPlace + 90, 84, 44, 'plot temp')
+  let tempLp = false
+  let tempLpCount = 0
+  let tempLpRun = async () => {
+    if (!tempLp) return
+    tempLpBtn.green('temp updating...')
+    try {
+      let temp = await vm.getExtruderTemp()
+      tempLpCount++
+      tempPlot.pushPt([tempLpCount, temp])
+      tempPlot.redraw()
+    } catch (err) {
+      tempLp = false
+      console.error(err)
+      tempLpBtn.red('temp update err, see console', 500)
+    }
+    if (pidDisplay) {
+      try {
+        let effort = await vm.getExtruderTempOutput()
+        effortPlot.pushPt([tempLpCount, effort])
+        effortPlot.redraw()
+      } catch (err) {
+        tempLp = false
+        console.error(err)
+        tempLpBtn.red('temp update err, see console', 500)
+      }
+    }
+    setTimeout(tempLpRun, 250)
+  }
+  tempLpBtn.onClick(() => {
+    if (tempLp) {
+      tempLp = false
+      tempLpBtn.grey('plot temp')
+    } else {
+      tempLp = true
+      tempLpRun()
+    }
+  })
+
+  if (pidDisplay) {
+    let pVal = new TextInput(xPlace, yPlace + 150, 87, 20, '-0.1')
+    let iVal = new TextInput(xPlace, yPlace + 180, 87, 20, '0.0')
+    let dVal = new TextInput(xPlace, yPlace + 210, 87, 20, '0.1')
+
+    let pidSetBtn = new EZButton(xPlace, yPlace + 240, 84, 14, 'set PID')
+    pidSetBtn.onClick(() => {
+      let p = parseFloat(pVal.value)
+      let i = parseFloat(iVal.value)
+      let d = parseFloat(dVal.value)
+      if (Number.isNaN(p) || Number.isNaN(i) || Number.isNaN(d)) {
+        pidSetBtn.bad("bad parse", 1000)
+        return
+      }
+      vm.setPIDTerms([p, i, d]).then(() => {
+        pidSetBtn.good("ok", 500)
+      }).catch((err) => {
+        console.error(err)
+        pidSetBtn.bad("err", 1000)
+      })
+    })
+  }
+
+  let pcfSetVal = 0;
+  if (pcfPresent){
+    let pcfBtn = new EZButton(xPlace, yPlace + 150, 84, 14, 'set pcf')
+    pcfBtn.onClick(() => {
+      if(pcfSetVal){
+        pcfSetVal = 0;
+      } else {
+        pcfSetVal = 1;
+      }
+      vm.setPCF(pcfSetVal).then(() => {
+        pcfBtn.good("ok")
+      }).catch((err) => {
+        console.error(err)
+        pcfBtn.bad("err")
+      })
+    })
+  }
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/toolBox.js b/system/javascript/osapjs/client/components/toolBox.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2166db3cb6a63ef7411affc23865a499bb89acc
--- /dev/null
+++ b/system/javascript/osapjs/client/components/toolBox.js
@@ -0,0 +1,103 @@
+/*
+toolBox.js
+
+toolchanger UI 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import { Button, EZButton } from '../interface/basics.js'
+
+export default function ToolBox(xPlace, yPlace, size, vm) {
+  let noToolBtn = Button(xPlace, yPlace, size - 6, size - 6, 'no tool')
+  // make buttons for each,
+  let buttons = []
+  let count = 0
+  for (let tool in vm.tools) {
+    count++
+    console.log(size * count)
+    console.log(count * 20)
+    buttons.push(Button(xPlace, yPlace + size * count + count * 10, size - 6, size - 6, tool))
+    console.log(vm.tools[tool])
+  }
+
+  let handler = async (evt) => {
+    // collect lable from the HTML... 
+    let toolRequest = evt.target.textContent
+    // make sure this is sensible, 
+    if (!vm.tools[toolRequest] && toolRequest != 'no tool') { throw new Error("tool button badness") }
+    // now... based on our current state, we try to make deltas: 
+    if (vm.currentTool.name == 'unknown') {
+      if (toolRequest != 'no tool') {
+        vm.currentTool = vm.tools[toolRequest]
+      } else {
+        vm.currentTool = { name: 'no tool' }
+      }
+    } else if (vm.currentTool.name == toolRequest) {
+      console.log("toolchange for same tool req'd")
+    } else {
+      // the rest of the statemachine is just in vm...
+      noToolBtn.yellow(`getting ${toolRequest} ...`)
+      try {
+        await vm.getTool(toolRequest)
+      } catch (err) {
+        console.error(err)
+        noToolBtn.red('tool err, see console')
+      }
+    }
+    // finally, update draw state?
+    this.updateDrawState()
+  }
+
+  // attach them 
+  noToolBtn.onClick(handler)
+  for (let btn in buttons) {
+    buttons[btn].onClick(handler)
+  }
+
+  this.updateDrawState = () => {
+    // color the btns according to machine's selected tool 
+    if (vm.currentTool.name == 'no tool') {
+      noToolBtn.green('no tool')
+    } else {
+      noToolBtn.grey('no tool')
+    }
+    for (let btn of buttons) {
+      if (btn.getText() == vm.currentTool.name) {
+        btn.green()
+      } else {
+        btn.grey()
+      }
+    }
+    // check which tool is present in machine-state and draw colors accordingly 
+  }
+
+  let stb = new EZButton(xPlace + size + 10, yPlace, size - 6, size - 6, "tc close")
+  stb.onClick(() => {
+    vm.toolChanger.setLeverState(true).then(() => {
+      stb.good("ok")
+    }).catch((err) => {
+      console.error(err)
+      stb.bad("err")
+    })
+  })
+
+  let stbo = new EZButton(xPlace + size + 10, yPlace + size + 10, size - 6, size - 6, "tc open")
+  stbo.onClick(() => {
+    vm.toolChanger.setLeverState(false).then(() => {
+      stbo.good("ok")
+    }).catch((err) => {
+      console.error(err)
+      stbo.bad("err")
+    })
+  })
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/components/torqueBox.js b/system/javascript/osapjs/client/components/torqueBox.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a27eb68bd2a9877c3a648fa64631daee9cee151
--- /dev/null
+++ b/system/javascript/osapjs/client/components/torqueBox.js
@@ -0,0 +1,215 @@
+/*
+torqueBox.js
+
+ye olden code from stepper motor by-effort application, 
+but contains useful examples 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { Button, TextInput } from '../interface/basics.js'
+
+function TorqueBox(xPlace, yPlace, vm) {
+    // jog 
+    let jogBtn = Button(xPlace, yPlace, 104, 104, 'click-in to jog')
+    let jogBigInput = TextInput(xPlace + 120, yPlace, 60, 20, '10.0')
+    let jogNormalInput = TextInput(xPlace + 120, yPlace + 30, 60, 20, '1.0')
+    let jogSmallInput = TextInput(xPlace + 120, yPlace + 60, 60, 20, '0.1')
+    let status = Button(xPlace + 120, yPlace + 90, 54, 14, '...')
+    // key status 
+    let zDown = false;
+    let setZ = (bool) => {
+        zDown = bool
+        if (zDown) {
+            $(status).text('z')
+        } else {
+            $(status).text('xy')
+            // do... 
+        }
+    }
+    let bigDown = false;
+    let setBig = (bool) => {
+        bigDown = bool
+        if (bigDown) {
+            setSmall(false)
+            setNormal(false)
+            $(jogBigInput).css('background-color', BTN_GRN)
+        } else {
+            $(jogBigInput).css('background-color', BTN_GREY)
+        }
+    }
+    let normalDown = false;
+    let setNormal = (bool) => {
+        normalDown = bool
+        if (normalDown) {
+            setSmall(false)
+            setBig(false)
+            $(jogNormalInput).css('background-color', BTN_GRN)
+        } else {
+            $(jogNormalInput).css('background-color', BTN_GREY)
+        }
+    }
+    let smallDown = false;
+    let setSmall = (bool) => {
+        smallDown = bool
+        if (smallDown) {
+            setNormal(false)
+            setBig(false)
+            $(jogSmallInput).css('background-color', BTN_GRN)
+        } else {
+            $(jogSmallInput).css('background-color', BTN_GREY)
+        }
+    }
+    // clear 
+    let noneDown = () => {
+        setNormal(false)
+        setBig(false)
+        setSmall(false)
+    }
+    // action
+    let parseOrReject = (numstr) => {
+        let val = parseFloat(numstr)
+        if (Number.isNaN(val)) {
+            return 0
+        } else {
+            return val
+        }
+    }
+    let getIncrement = () => {
+        let val = 0
+        console.log(smallDown, normalDown, bigDown)
+        if (smallDown) {
+            return parseOrReject(jogSmallInput.value)
+        } else if (normalDown) {
+            return parseOrReject(jogNormalInput.value)
+        } else if (bigDown) {
+            return parseOrReject(jogBigInput.value)
+        } else {
+            console.error('no increment selected, statemachine borked')
+            return 0
+        }
+    }
+    let jog = (key) => {
+        let tq = getIncrement()
+        switch (key) {
+            case 'left':
+                vm.setTorque(-tq)
+                break;
+            case 'right':
+                vm.setTorque(tq)
+                break;
+            default:
+                console.error("bad key", key)
+        }
+    }
+    // key listeners 
+    this.keyDownListener = (evt) => {
+        if (evt.repeat) return
+        //console.log('keydown', evt.keyCode)
+        switch (evt.keyCode) {
+            case 90:
+                setZ(true)
+                break;
+            case 88:
+                setBig(true)
+                break;
+            case 67:
+                setSmall(true)
+                break;
+            case 38:
+                jog('up')
+                break;
+            case 40:
+                jog('down')
+                break;
+            case 37:
+                jog('left')
+                break;
+            case 39:
+                jog('right')
+                break;
+            default:
+                break;
+        }
+    }
+    // up 
+    this.keyUpListener = (evt) => {
+        //console.log('keyup', evt.keyCode)
+        switch (evt.keyCode) {
+            case 37:
+            case 39:
+            case 40:
+            case 38:
+                vm.setTorque(0)
+                break;
+            case 90:
+                setZ(false);
+                break;
+            case 88:
+                setBig(false)
+                setNormal(true)
+                break;
+            case 67:
+                setSmall(false)
+                setNormal(true)
+                break;
+            default:
+                break;
+        }
+    }
+    // in-to-state
+    this.start = () => {
+        jogBtn.clicked = true
+        $(jogBtn).css('background-color', BTN_GRN)
+        $(jogBtn).html('x: big<br>c: small<br>z: map y to z')
+        $(status).text('xy')
+        document.addEventListener('keydown', this.keyDownListener)
+        document.addEventListener('keyup', this.keyUpListener)
+        if (bigDown) {
+            setBig(true)
+        } else if (smallDown) {
+            setSmall(true)
+        } else {
+            setNormal(true)
+        }
+        if (zDown) {
+            $(status).text('z')
+        }
+    }
+    // out-of 
+    this.stop = () => {
+        jogBtn.clicked = false
+        $(jogBtn).html('click-in to jog')
+        $(jogBtn).css('background-color', BTN_GREY)
+        $(status).text('...')
+        noneDown()
+        document.removeEventListener('keydown', this.keyDownListener)
+        document.removeEventListener('keyup', this.keyUpListener)
+    }
+    // restart w/ varied button-down state 
+    this.restart = () => {
+        if (jogBtn.clicked) {
+            this.start()
+        } else {
+            this.stop()
+        }
+    }
+    // go big 
+    this.select
+    // ok, statemachine 
+    $(jogBtn).on('click', (evt) => {
+        if (!jogBtn.clicked) {
+            this.start()
+        } else {
+            this.stop()
+        }
+    })
+}
+
+export { TorqueBox }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/doodler/graphicalElements.js b/system/javascript/osapjs/client/doodler/graphicalElements.js
new file mode 100644
index 0000000000000000000000000000000000000000..a699c8203100abb66585d66ff5ca39d79beed25c
--- /dev/null
+++ b/system/javascript/osapjs/client/doodler/graphicalElements.js
@@ -0,0 +1,358 @@
+import { html, svg, render } from 'https://unpkg.com/lit-html?module';
+import { VT } from '../../core/ts.js';
+import DT from '../interface/domTools.js'
+
+// everything here should have a constructor for some virtual element...
+// and a delete fn 
+
+// drawing consts... make-actual, 
+let gap = 5, padding = 10;
+
+let getCenter = (gvt) => {
+  let st = gvt.state 
+  return { x: st.x + st.width/2 + padding, y: st.y + st.height/2 + padding }
+}
+
+let getLeftEdge = (gvt) => {
+  let st = gvt.state
+  return { x: st.x, y: st.y + st.height / 2 + padding }
+}
+
+let getRightEdge = (gvt) => {
+  let st = gvt.state
+  return { x: st.x + st.width + 2 * padding, y: st.y + st.height / 2 + padding }
+}
+
+let checkGvtOverlap = (a, b) => {
+  a = a.state; b = b.state
+  // calc like
+  if( a.x < (b.x + b.width + 2 * padding) &&
+      (a.x + a.width + 2 * padding) > b.x &&
+      a.y < (b.y + b.height + 2 * padding) && 
+      (a.y + a.height + 2 * padding) > b.y){
+    return true 
+  } else {
+    return false 
+  }
+}
+
+// graph element for vertex element... 
+function GraphicalContext(virtualVertex) {
+  // everything has its 
+  this.vvt = virtualVertex
+  // and some state,
+  this.state = { x: 0, y: 0, width: 150, height: 20 }
+  // each element goes in dummy container (we could tag this, right?)
+  // this is kinda garbo but lit-html will handle one template per container, so here we are 
+  let cont = $(`<div>`).get(0)
+  $($('.plane').get(0)).append(cont)
+  // we have a global uuid (fn on muleClient.js) 
+  this.uuid = window.nd.getNewElementUUID()
+  // we are a lit element 
+  let template = (self) => html`
+  <div class="ddlrElement gvtRoot" id="${self.uuid}"
+    style = "width: ${self.state.width}px; height: ${self.state.height}px; padding: ${padding}px;
+    transform: scale(1); left: ${self.state.x}px; top: ${self.state.y}px;">
+    ${self.vvt.name}
+  </div>
+  `
+  // make some children...
+  this.children = []
+  for (let vvt of this.vvt.children) {
+    let gvt = {}
+    if(vvt.type == VT.VPORT){
+      gvt = new GraphicalVPort(vvt)
+    } else if (vvt.type == VT.ENDPOINT){
+      gvt = new GraphicalEndpoint(vvt)
+    } else {
+      gvt = new GraphicalVertex(vvt)
+    }
+    vvt.gvt = gvt; gvt.vvt = vvt;
+    this.children.push(gvt)
+  }
+  // has render call, 
+  this.render = () => {
+    render(template(this), cont)
+    for (let c in this.children) {
+      this.children[c].state.x = this.state.x + 10
+      this.children[c].state.y = this.state.y + this.state.height + gap + padding * 2 + (this.children[c].state.height + gap + padding * 2) * parseInt(c)
+      this.children[c].render()
+    }
+  }
+  // can rm thing 
+  this.delete = () => {
+    $(cont).remove()
+    for (let child of this.children) { child.delete() }
+  }
+  // we get added to global list, 
+  window.nd.gvts.push(this)
+}
+
+function GraphicalVertex(virtualVertex) {
+  console.warn(`init generic gvertex ${virtualVertex.name}`)
+  this.vvt = virtualVertex
+  let ogBackground = "rgb(205, 205, 205)"
+  this.state = { x: 0, y: 0, width: 140, height: 20, text: this.vvt.name, backgroundColor: "rgb(205, 205, 205)" }
+  // render into... 
+  let cont = $('<div>').get(0)
+  $($('.plane').get(0)).append(cont)
+  // we also have uuid, 
+  this.uuid = window.nd.getNewElementUUID()
+  // have a template, 
+  let template = (self) => html`
+  <div class="ddlrElement gvtVertex" id="${self.uuid}"
+    style = "width: ${self.state.width}px; height: ${self.state.height}px; padding: ${padding}px;
+    transform: scale(1); left: ${self.state.x}px; top: ${self.state.y}px;
+    background-color:${self.state.backgroundColor}">
+    ${self.state.text}
+  </div>
+  `
+  // utes,
+  this.setBackgroundColor = (color) => {
+    if (color) {
+      this.state.backgroundColor = color
+    } else {
+      this.state.backgroundColor = ogBackground
+    }
+    this.render()
+  }
+  this.setText = (text) => {
+    this.state.text = text 
+    this.render() 
+  }
+  // render call, 
+  this.render = () => {
+    render(template(this), cont)
+  }
+  // can rm thing,
+  this.delete = () => {
+    $(cont).remove()
+  }
+  // we get added to global list,
+  window.nd.gvts.push(this)
+}
+
+function GraphicalVPort(virtualVertex) {
+  // everything has its 
+  this.vvt = virtualVertex
+  // and some state,
+  let ogBackground = "rgb(205, 205, 205)"
+  this.state = { x: 0, y: 0, width: 140, height: 20, backgroundColor: ogBackground }
+  // render into... 
+  let cont = $('<div>').get(0)
+  $($('.plane').get(0)).append(cont)
+  // we also have uuid, 
+  this.uuid = window.nd.getNewElementUUID()
+  // have a template, 
+  let template = (self) => html`
+  <div class="ddlrElement gvtVPort" id="${self.uuid}"
+    style = "width: ${self.state.width}px; height: ${self.state.height}px; padding: ${padding}px;
+    transform: scale(1); left: ${self.state.x}px; top: ${self.state.y}px;
+    background-color:${self.state.backgroundColor}">
+    ${self.vvt.name}
+  </div>
+  `
+  // find our partners... 
+  this.pipes = []
+  this.linkSetup = () => {
+    if (this.vvt.reciprocal && this.vvt.reciprocal.type != "unreachable") {
+      // check if partner already has one-of-us hooked up, 
+      for (let pipe of this.vvt.reciprocal.gvt.pipes) {
+        if (pipe.tail = this) {
+          return
+        }
+      }
+      // if no existing hookup, we are the head... 
+      let pipe = new GraphicalLink([this, this.vvt.reciprocal.gvt])
+      this.pipes.push(pipe)
+      // and put that pipe into the friend-pipes-list as well, so it will rerender on their move... 
+      this.vvt.reciprocal.gvt.pipes.push(pipe)
+    }
+  }
+  // utes,
+  this.setBackgroundColor = (color) => {
+    if (color) {
+      this.state.backgroundColor = color
+    } else {
+      this.state.backgroundColor = ogBackground
+    }
+    this.render()
+  }
+  // render call, 
+  this.render = () => {
+    render(template(this), cont)
+    for (let pipe of this.pipes) {
+      pipe.render()
+    }
+  }
+  // can rm thing,
+  this.delete = () => {
+    $(cont).remove()
+    for (let pipe of this.pipes) { pipe.delete() }
+  }
+  // we get added to global list,
+  window.nd.gvts.push(this)
+}
+
+function GraphicalEndpoint(virtualVertex){
+  // everything has its 
+  this.vvt = virtualVertex
+  // and some state,
+  let ogBackground = "rgb(205, 205, 205)"
+  this.state = { x: 0, y: 0, width: 140, height: 20, backgroundColor: ogBackground }
+  // render into... 
+  let cont = $('<div>').get(0)
+  $($('.plane').get(0)).append(cont)
+  // we also have uuid, 
+  this.uuid = window.nd.getNewElementUUID()
+  // have a template, 
+  let template = (self) => html`
+  <div class="ddlrElement gvtEndpoint" id="${self.uuid}"
+    style = "width: ${self.state.width}px; height: ${self.state.height}px; padding: ${padding}px;
+    transform: scale(1); left: ${self.state.x}px; top: ${self.state.y}px;
+    background-color:${self.state.backgroundColor}">
+    ${self.vvt.name}
+  </div>
+  `
+  // find our partners... 
+  this.pipes = []
+  this.linkSetup = () => {
+    for(let r in this.vvt.routes){
+      let route = this.vvt.routes[r]
+      let walk = window.osap.netRunner.routeWalk(route, this.vvt)
+      console.warn('WALKED', walk)
+      try {
+        walk.path = walk.path.map(x => x.gvt)
+      } catch (err){
+        console.error("bad map from vvt to gvts for path drawing")
+        console.error(err)
+      }
+      //console.log('drawing thru...', walk)
+      // init new, 
+      let pipe = new GraphicalRoute(walk.path, r, walk.state == "complete")
+      // add to us & them... 
+      // console.warn(walk.state)
+      this.pipes.push(pipe)
+      walk.path[walk.path.length - 1].pipes.push(pipe)
+    }
+  }
+  // utes,
+  this.setBackgroundColor = (color) => {
+    if (color) {
+      this.state.backgroundColor = color
+    } else {
+      this.state.backgroundColor = ogBackground
+    }
+    this.render()
+  }
+  // render call, 
+  this.render = () => {
+    render(template(this), cont)
+    for (let pipe of this.pipes) {
+      pipe.render()
+    }
+  }
+  // can rm thing,
+  this.delete = () => {
+    $(cont).remove()
+    for (let pipe of this.pipes) { pipe.delete() }
+  }
+  // we get added to global list,
+  window.nd.gvts.push(this)
+}
+
+// having a list of virtual vertices to pass through... each item in path should be a gvt, 
+function GraphicalRoute(path, indice, completeness) {
+  // aye,
+  this.isRoute = true
+  this.indice = parseInt(indice)
+  // kinda hackney all-consuming SVG canvas, 
+  let cont = $('<div style="position:absolute; z-index:0; overflow:visible;"></div>').get(0)
+  $($('.plane').get(0)).append(cont)
+  // we also have uuid, 
+  this.uuid = window.nd.getNewElementUUID()
+  // we track... head gvt and tail gt 
+  this.head = path[0]
+  this.tail = path[path.length - 1]
+  this.midpts = path.slice(1, -1)
+  this.state = { color: "rgb(150,200,150)", strokeWidth: 5 } // color, etc 
+  if(completeness) {
+    this.state.color = "rgb(150, 150, 200)"
+  } else {
+    this.state.color = "rgb(230, 180, 180)"
+  }
+  let template = (self) => {
+    let head = getRightEdge(self.head); 
+    let tail = completeness ? getLeftEdge(self.tail) : {x : head.x + 25, y: head.y }; 
+    // we'd like to make the shortest path, 
+    return svg`
+    <svg width="10" height="10" style="position:absolute; z-index:0; overflow:visible;" xmlns:xlink="http://w3.org/1999/xlink">
+      <g>
+      <path class="svgRoute" id="${self.uuid}" d="
+        M ${head.x} ${head.y} C 
+        ${completeness ? head.x + 100 : head.x + 10} ${head.y} 
+        ${completeness ? tail.x - 100 : tail.x - 10} ${tail.y}
+        ${tail.x} ${tail.y}"
+        stroke="${self.state.color}" fill="none" stroke-width="${self.state.strokeWidth}"></path>
+      <polygon points="${head.x - 4},${head.y + 12} ${head.x + 17},${head.y} ${head.x - 4}, ${head.y - 12}" fill="${self.state.color}"></polygon>
+      <rect x="${tail.x - 8}" y="${tail.y - 8}" width="16" height="16" rx="3" ry="3" fill="${self.state.color}"></rect>
+      </g>
+    </svg>
+    `
+  }
+  this.render = () => {
+    render(template(this), cont)
+  }
+  this.delete = () => {
+    $(cont).remove()
+  }
+  // we get added to global list, 
+  window.nd.gvts.push(this)
+}
+
+// having a list of virtual vertices to pass through... each item in path should be a gvt, 
+function GraphicalLink(path) {
+  // kinda hackney all-consuming SVG canvas, 
+  let cont = $('<div style="position:absolute; z-index:0; overflow:visible;"></div>').get(0)
+  $($('.plane').get(0)).append(cont)
+  // we track... head gvt and tail gt 
+  this.head = path[0]
+  this.tail = path[path.length - 1]
+  this.midpts = path.slice(1, -1)
+  this.state = { color: "rgb(180,180,180)", strokeWidth: 5 } // color, etc 
+  let template = (self) => {
+    let headMid = getCenter(self.head); let tailMid = getCenter(self.tail)
+    let head = {}, tail = {} 
+    if(headMid.x > tailMid.x){ // 'head' and 'tail' aren't so important for drawing, but which edge to pickup is 
+      head = getRightEdge(self.tail); tail = getLeftEdge(self.head)
+    } else {
+      head = getRightEdge(self.head); tail = getLeftEdge(self.tail)
+    }
+    // we'd like to make the shortest path, 
+    return svg`
+    <svg width="10" height="10"  style="position:absolute; z-index:0; overflow:visible;" xmlns:xlink="http://w3.org/1999/xlink">
+      <g>
+      <path d="
+        M ${head.x} ${head.y} C 
+        ${head.x + 100} ${head.y} 
+        ${tail.x - 100} ${tail.y}
+        ${tail.x} ${tail.y}"
+        stroke="${self.state.color}" fill="none" stroke-width="${self.state.strokeWidth}" stroke-dasharray="8 4"></path>
+      <circle r="7" cx="${head.x}" cy="${head.y}" fill="${self.state.color}"></circle>
+      <circle r="7" cx="${tail.x}" cy="${tail.y}" fill="${self.state.color}"></circle>
+      </g>
+    </svg>
+    `
+  }
+  this.render = () => {
+    render(template(this), cont)
+  }
+  this.delete = () => {
+    $(cont).remove()
+  }
+  // we get added to global list, 
+  window.nd.gvts.push(this)
+}
+
+
+export { GraphicalContext, GraphicalVertex, checkGvtOverlap }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/doodler/gridDoodler.js b/system/javascript/osapjs/client/doodler/gridDoodler.js
new file mode 100644
index 0000000000000000000000000000000000000000..a74007c4b8a04aa89fe7c2651d5d0299510230dd
--- /dev/null
+++ b/system/javascript/osapjs/client/doodler/gridDoodler.js
@@ -0,0 +1,94 @@
+/*
+gridDoodler.js
+
+tool to draw *grids* explicitly (!) not meshes 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from '../interface/domTools.js'
+import { VT } from '../../core/ts.js'
+import TIME from '../../core/time.js'
+
+let padSize = 150 // square 
+let padPadding = 10
+
+export default function GridDoodler(xPlace, yPlace) {
+  this.redraw = (graph) => {
+    // rm all old, 
+    $('.node').remove()
+    // for this one, we should recurse by root, right?
+    let drawTime = TIME.getTimeStamp()
+    let recursor = (root, x, y) => {
+      // don't draw forever 
+      if (root.lastDrawTime == drawTime){
+        console.log(`avoids redrawing ${root.name}`)
+        return;
+      }
+      root.lastDrawTime = drawTime
+      // I guess I want some idea of a shape... 
+      // draw this pad, 
+      this.drawPad(x, y, root.name)
+      // check vports, 
+      for (let c = 0; c < root.children.length; c ++) {
+        if (root.children[c].type == VT.VPORT) {
+          // could draw the actual vport, then:
+          if (root.children[c].reciprocal && root.children[c].reciprocal.parent) {
+            // let's see... 0, 1 draw *above*
+            // but this depends on our orientation, right? 
+            let nx = x
+            let ny = y
+            if(root.name == "embedded-root"){
+              switch (c) {
+                case 0:
+                case 1:
+                  ny--
+                  break;
+                case 2:
+                  nx--
+                  break;
+                case 3:
+                  ny++
+                  break;
+                case 4:
+                  nx++
+                  break;
+                default:
+                  console.warn("wut?")
+              }  
+            } else {
+              switch(c){
+                case 0: 
+                  ny ++;
+                  break;
+                case 1: 
+                  ny ++;
+                  break;
+                default: 
+                  console.warn("what?")
+              }
+            }
+            recursor(root.children[c].reciprocal.parent, nx, ny)
+            // assuming same orientation, 1 is north, 2 west, 3 south, 4 east 
+          }
+        }
+      }
+    }
+    recursor(graph, 0, 0)
+  }
+
+  this.drawPad = (x, y, name = "pad") => {
+    let pad = $('<div>').addClass('node').text(name).get(0)
+    DT.placeField(pad, padSize, padSize,
+      xPlace + x * padSize + x * padPadding,
+      yPlace + y * padSize + y * padPadding)
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/doodler/netDoodler.js b/system/javascript/osapjs/client/doodler/netDoodler.js
new file mode 100644
index 0000000000000000000000000000000000000000..209f354d3b2352d04d98fd202404ce569aad1a19
--- /dev/null
+++ b/system/javascript/osapjs/client/doodler/netDoodler.js
@@ -0,0 +1,438 @@
+/*
+netDoodler.js
+
+osap tool drawing set
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from '../interface/domTools.js'
+import { Button, TextBlock } from '../interface/basics.js'
+import { VT } from '../../core/ts.js'
+import TIME from '../../core/time.js'
+import PK from '../../core/packets.js'
+import { GraphicalContext, GraphicalVertex, checkGvtOverlap } from './graphicalElements.js';
+
+// get dom:gvt match
+let getGvtByUUID = (uuid) => {
+  for (let cand of window.nd.gvts) {
+    if (cand.uuid == parseInt(uuid)) {
+      return cand
+    }
+  }
+  return null
+}
+
+// global mouse listener, w/ also one in ../interface/grid.js
+window.addEventListener('mousedown', (evt) => {
+  //console.log(evt.target)
+  // it's us? 
+  if (!($(evt.target).is('.gvtRoot') || $(evt.target).is('.gvtEndpoint') || $(evt.target).is('.svgRoute') )) {
+    return;
+  }
+  // see if we can't get the gvx... 
+  let id = $(evt.target).attr('id')
+  //console.warn('out for...', id)
+  // can we find it ?
+  let gvt = getGvtByUUID(id)
+  if (!gvt) { console.warn('no gvt found on drag'); return }
+  // gottem 
+  evt.preventDefault(); evt.stopPropagation();
+  // want these in any case, 
+  let ogmx = evt.clientX; let ogmy = evt.clientY;
+  let oggx = gvt.state.x; let oggy = gvt.state.y;
+  let scale = DT.readTransform($('.plane').get(0)).s
+  // do drag-or-endpoint, 
+  if(gvt.isRoute){
+    // ayl-wroight then, we need to figure which indice of which route this is... 
+    let head = gvt.head
+    let indice = gvt.indice 
+    // then we shooould be able to...
+    window.osap.mvc.removeEndpointRoute(head.vvt.route, indice).then((res) => {
+      console.warn('route delete completes', res)
+      window.nd.stateTransition('scanning')
+    }).catch((err) => {
+      console.error(err)
+    })
+  } else if (gvt.vvt.type == VT.ROOT) { // or drag on endpoint... 
+    // if state transition OK, set drag... 
+    if (window.nd.stateTransition('dragging')) {
+      // set drag handler, 
+      DT.dragTool((drag) => {
+        let delx = (drag.clientX - ogmx) / scale; let dely = (drag.clientY - ogmy) / scale;
+        gvt.state.x = oggx + delx; gvt.state.y = oggy + dely;
+        gvt.render()
+      }, (up) => { window.nd.stateTransition('idle') })
+    } else {
+      return
+    }
+  } else if (gvt.vvt.type == VT.ENDPOINT) {
+    if (window.nd.stateTransition('dragging')) {
+      // set og gvt to new color, 
+      gvt.setBackgroundColor("rgb(200, 200, 250)")
+      gvt.render()
+      // splash one new blerk, 
+      let tempGvt = new GraphicalVertex({ name: "<type>" })
+      tempGvt.state.x = oggx; tempGvt.state.y = oggy
+      tempGvt.state.backgroundColor = "rgb(150, 150, 200)"
+      tempGvt.state.width = 100; tempGvt.state.height = 15;
+      tempGvt.render()
+      // we'll want to rm this thing...
+      let rmFloater = () => {
+        // rm floater, 
+        let indice = window.nd.gvts.findIndex((cand) => { return cand.uuid == tempGvt.uuid })
+        if (!indice) {
+          console.error(`couldn't find floater gvt to remove`)
+        } else {
+          tempGvt.delete()
+          window.nd.gvts.splice(indice, 1)
+        }
+      }
+      let lastCand = null
+      // setup for drags / overlap checks, 
+      let delx = 0, dely = 0 // xy move deltas,
+      DT.dragTool((drag) => {
+        // rerender floater, 
+        delx = (drag.clientX - ogmx) / scale; dely = (drag.clientY - ogmy) / scale;
+        tempGvt.state.x = oggx + delx; tempGvt.state.y = oggy + dely;
+        tempGvt.render()
+        // check if floater is within bounds of any other endpoints... is this overkill? it's unclear
+        let pos = { x: tempGvt.state.x, y: tempGvt.state.y }
+        // rerender old...
+        if (lastCand) {
+          lastCand.setBackgroundColor();
+          lastCand.render()
+          lastCand = null
+        }
+        for (let cand of window.nd.gvts) {
+          if (!cand.vvt) continue;
+          if (cand.vvt.type != VT.ENDPOINT) continue;
+          if (cand == gvt) continue;
+          if (checkGvtOverlap(tempGvt, cand)) {
+            cand.setBackgroundColor("rgb(200, 200, 250")
+            cand.render()
+            lastCand = cand
+            return
+          }
+        }
+      }, (up) => {
+        window.nd.stateTransition('idle')
+        if (lastCand) {
+          let route = window.osap.netRunner.findRoute(gvt.vvt, lastCand.vvt)
+          if (!route) {
+            console.error(`bad route traversal ${gvt.vvt.name} -> ${lastCand.vvt.name}`)
+          } else {
+            console.warn('found route', route)
+            tempGvt.setText('req route...')
+            window.osap.mvc.setEndpointRoute(gvt.vvt.route, route).then(() => {
+              tempGvt.setText('success...')
+              // trigger a scan now, if we can, 
+              window.nd.stateTransition("scanning")
+              tempGvt.setBackgroundColor("rgb(200, 250, 200)")
+              setTimeout(() => {
+                rmFloater()
+              }, 250)
+            }).catch((err) => {
+              tempGvt.setText('err...')
+              tempGvt.setBackgroundColor("rgb(250, 200, 200)")
+              console.error(err)
+              setTimeout(() => {
+                rmFloater()
+              }, 250)
+            })
+          } // end lift-with-route, 
+          // reset dom stuff, 
+          lastCand.setBackgroundColor()
+          gvt.setBackgroundColor()
+          // floater should go...
+          tempGvt.setBackgroundColor("rgb(250, 200, 200)")
+          return
+        } else {
+          rmFloater()
+        }
+      })
+    } else {
+      return
+    }
+  }
+})
+
+// try hover listeners, we have to re-register these after each render, wherp 
+let registerHandlers = () => {
+  // hover listener, 
+  $('.gvtVPort').hover((enter) => {
+    //console.warn('hov')
+    let gvt = getGvtByUUID($(enter.target).attr('id'))
+    if (!gvt) { console.error('no gvt on gvtVPort hover entrance'); return }
+    if (gvt.vvt.reciprocal && gvt.vvt.reciprocal.type != "unreachable") {
+      gvt.setBackgroundColor(`rgb(180, 180, 180)`)
+      gvt.vvt.reciprocal.gvt.setBackgroundColor(`rgb(180, 180, 180)`)
+    }
+  }, (exit) => {
+    let gvt = getGvtByUUID($(exit.target).attr('id'))
+    if (!gvt) { console.error('no gvt on gvtVPort hover exit'); return }
+    if (gvt.vvt.reciprocal && gvt.vvt.reciprocal.type != "unreachable") {
+      gvt.setBackgroundColor()
+      gvt.vvt.reciprocal.gvt.setBackgroundColor()
+    }
+  })
+  // hover routes, 
+  $('.svgRoute').hover((enter) => {
+    let gvt = getGvtByUUID($(enter.target).attr('id'))
+    gvt.state.color = "rgb(200, 150, 200)"
+    gvt.render()
+  }, (exit) => {
+    let gvt = getGvtByUUID($(exit.target).attr('id'))
+    gvt.state.color = "rgb(150, 150, 200)"
+    gvt.render()
+  })
+}
+
+
+export default function NetDoodler(osap, xPlace, yPlace, _runState = true) {
+  // -------------------------------------------- ND STATE MANAGE 
+  // we have some basic controls here, 
+  let runState = _runState
+  let checkRunState = () => {
+    if (runState) {
+      runBtn.green(); this.stateTransition("scanning")
+    } else {
+      runBtn.red();
+    }
+  }
+  let runBtn = new Button(xPlace, yPlace, 84, 84, 'loop?')
+  runBtn.onClick((evt) => {
+    runState = !runState
+    checkRunState()
+  })
+  // and a display of our current state, 
+  let stateDisplay = new TextBlock(xPlace, yPlace + 100, 84, 40, 'idle')
+  let writeState = (state) => {
+    this.state = state
+    stateDisplay.setText(state)
+  }
+  writeState('idle')
+  // tiny ute, 
+  this.awaitIdle = () => {
+    return new Promise((resolve, reject) => {
+      let check = () => { this.state == idle ? resolve() : setTimeout(check, 50) }
+      check()
+      setTimeout(() => { reject("awaitIdle timeout"), 5000 })
+    })
+  }
+  // state machine transitions... returns true if legal transit 
+  let scanTimer = null
+  this.stateTransition = (target, arg) => {
+    // console.log(`${this.state} -> ${target}`)
+    try {
+      if (this.state == "idle" && target == "scanning") {
+        writeState("scanning")
+        osap.netRunner.sweep().then((net) => {
+          console.warn(`SWEEP returns`, net)
+          writeState("drawing")
+          this.stateTransition("idle")
+          return true 
+          osap.mvc.fillRouteData(net).then((net) => {
+            console.warn(`FILLROUTE returns`, net)
+            this.stateTransition("drawing", net)
+          }).catch((err) => { // route collect error, more common 
+            console.error(err)
+            writeState('idle')
+            runState = false 
+            checkRunState()
+            //this.stateTransition("scanning")
+          })
+        }).catch((err) => { // sweep error 
+          console.error(err)
+          writeState('error')
+        })
+        return true
+      } else if (this.state == "scanning" && target == "scanning") {
+        return false;
+      } else if (this.state == "scanning" && target == "drawing") {
+        writeState("drawing")
+        this.redraw(arg).then(() => {
+          this.stateTransition("idle")
+        }).catch((err) => {
+          console.error(err)
+          this.stateTransition("error")
+        })
+        return true
+      } else if ((this.state == "drawing" || this.state == "dragging") && target == "idle") {
+        writeState("idle")
+        if (runState && !scanTimer) {
+          scanTimer = setTimeout(() => { scanTimer = null; this.stateTransition("scanning") }, 2000)
+        }
+        return true
+      } else if (this.state == "drawing" && target == "dragging") {
+        simulation.stop();
+        writeState("dragging");
+        return true;
+      } else if (this.state == "idle" && target == "drawing") {
+        // seems bad, 
+        return false;
+      } else if ((this.state == "idle" || this.state == "scanning") && target == "dragging") {
+        writeState("dragging")
+        return true
+      } else if (this.state == "dragging" && (target == "scanning" || target == "drawing")) {
+        return false
+      } else if (target == "error") {
+        runState = false; checkRunState()
+        writeState("error");
+      } else {
+        console.error(`unknown state transition from ${this.state} to ${target}`)
+        this.stateTransition("error")
+        return false
+      }
+    } catch (err) {
+      writeState("error")
+      console.error(err)
+    }
+  }
+  checkRunState()
+  // -------------------------------------------- ND UTES 
+  let lastUUID = 0
+  this.getNewElementUUID = () => {
+    return lastUUID++
+  }
+
+  // there is a map between simulation posns and drawing posns, since 
+  // we can't move spawn origin for d3 sim, ffs, https://observablehq.com/@d3/force-layout-phyllotaxis 
+  let simOffset = 500
+  this.gvts = []
+  // first we want to diff the graph, and get a copy of it in node:links form, for D3 
+  // we get a new graph every redraw call, but have an existing copy... 
+  this.redraw = async (graph) => {
+    // position state is the only thing to maintain, everything else gets wiped 
+    let posns = []
+    for (let gvt of this.gvts) {
+      if (gvt.vvt && gvt.vvt.type == VT.ROOT) {
+        posns.push({
+          route: gvt.vvt.route,
+          x: gvt.state.x,
+          y: gvt.state.y
+        })
+      }
+    }
+    // rm old gvts, 
+    for (let gvt of this.gvts) {
+      gvt.delete()
+    }
+    this.gvts = []
+    // we'll populate these recursively... 
+    let nodes = []; let links = []
+    let lastId = 1;
+    let drawTime = TIME.getTimeStamp()
+    // let's just walk the graph and try our new rendering tech, 
+    let contextRecursor = (vvt, partner = undefined) => {
+      // guard against whatever tf this is ?
+      if(!vvt){
+        console.warn('no vvt here on redraw recurse ??'); return;
+      }
+      // don't recurse back up, 
+      if (vvt.lastDrawTime && vvt.lastDrawTime == drawTime) return;
+      vvt.lastDrawTime = drawTime
+      // make a node for this thing, and an element for ourselves, 
+      let gvt = new GraphicalContext(vvt) // won't exist until we render it 
+      // make a simulation node, 
+      let node = { id: lastId++, name: vvt.name, index: nodes.length, vvt: vvt, gvt: gvt }
+      nodes.push(node)
+      // everything is everything 
+      gvt.node = node;
+      vvt.gvt = gvt; vvt.node = node;
+      // we have a new node, a new gvt, 
+      // if there's an element in the old gvts for this node, set fixed posn 
+      for (let pos of posns) {
+        if (PK.routeMatch(pos.route, gvt.vvt.route)) {
+          // console.log('found same!')
+          node.fx = pos.x - simOffset
+          node.fy = pos.y - simOffset
+          break;
+        }
+      }
+      // add link if it exists 
+      if (partner) links.push({ source: partner, target: node })
+      // sweep thru vports 
+      for (let c = 0; c < vvt.children.length; c++) {
+        if (vvt.children[c].type == VT.VPORT) {
+          let vp = vvt.children[c]
+          if (vp.reciprocal && vp.reciprocal.type != "unreachable") contextRecursor(vp.reciprocal.parent, node)
+        }
+      }
+    }
+    // kick it w/ root as root... 
+    try {
+      contextRecursor(graph)
+    } catch (err) {
+      console.error(err)
+    }
+    // now we have a fresh set of gvts, for which we want to run, 
+    for (let gvt of this.gvts) {
+      if (gvt.linkSetup) gvt.linkSetup()
+    }
+    // stuff 1st node to 0,0 if it's new
+    if (nodes[0] && !nodes[0].fx) {
+      nodes[0].fx = - simOffset + 200; nodes[0].fy = - simOffset + 50;
+    }
+    // do we need to use d3 ?
+    let useSim = false
+    for (let gvt of this.gvts) {
+      if (gvt.node && gvt.node.fx == undefined) {
+        useSim = true; break;
+      }
+    } // end check for newshit 
+    try {
+      await this.settleNodes({ nodes: nodes, links: links }, useSim)
+    } catch (err) {
+      console.error(err)
+    }
+  } // end this.redraw 
+
+  // data here is like: { nodes: [ { id: <num>, name: <string>, index: indx } ], links: [ {source: <obj in nodes list>, target: <obj in nodes list>, index: indx } ] }
+  let simulation = null
+  this.settleNodes = (data, settle) => {
+    return new Promise((resolve, reject) => {
+      // Let's list the force we wanna apply on the network
+      simulation = d3.forceSimulation(data.nodes)                 // Force algorithm is applied to data.nodes
+        .force("link", d3.forceLink()                               // This force provides links between nodes
+          .id(function (d) { return d.id; })                     // This provide  the id of a node
+          .links(data.links)                                    // and this the list of links
+          .distance(function (d) { return 300; })
+        )
+        .force("charge", d3.forceManyBody().strength(-800))         // This adds repulsion between nodes. Play with the -400 for the repulsion strength
+        //.force("center", d3.forceCenter(width / 2, height / 2))     // This force attracts nodes to the center of the svg area
+        .alphaMin(0.1)
+        .on("tick", ticked)
+        .on("end", completion);
+
+      // it's a mess, but after each 1st-cycle we want to register global handlers, so need:
+      let first = true
+      // This function is run at each iteration of the force algorithm, updating the nodes position.
+      function ticked() {
+        try {
+          for (let node of data.nodes) {
+            node.gvt.state.x = node.x + simOffset
+            node.gvt.state.y = node.y + simOffset
+            node.gvt.render()
+          }
+          if (first) {
+            registerHandlers()
+            first = false
+          }
+          // stop after one tick / update cycle if we don't need to sim... 
+          if (!settle) { simulation.stop(); simulation = null; resolve() }
+        } catch (err) { simulation.stop(); simulation = null; reject(err) }
+      }
+
+      function completion() { simulation = null; resolve() }
+    })
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/interface/basics.js b/system/javascript/osapjs/client/interface/basics.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d51e6a3ca94b1dc34fd0b5749553b73cecb2469
--- /dev/null
+++ b/system/javascript/osapjs/client/interface/basics.js
@@ -0,0 +1,210 @@
+/*
+button.js
+
+for real, a button class
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from './domTools.js'
+import style from './style.js'
+
+// (svg)esus
+function svgRenderer(xPlace, yPlace, width, height, svg){
+  let elem = $('<div>').css('position', 'absolute').get(0)
+  $(elem).append(svg) 
+  DT.placeField(elem, width, height, xPlace, yPlace)
+}
+
+// single action buttons, 
+function EZButton(xPlace, yPlace, width, height, text) {
+  let elem = $('<div>').addClass('button')
+    .text(text)
+    .get(0)
+  DT.placeField(elem, width - 6, height - 6, xPlace, yPlace)
+  let btn = {}
+  btn.onClick = (fn) => {
+    $(elem).on('click', (evt) => {
+      fn()
+      $(elem).text('...').css('background-color', style.ylw)
+    })
+  }
+  btn.good = (msg, time) => {
+    if (!time) time = 500
+    $(elem).text(msg).css('background-color', style.grn)
+    setTimeout(() => {
+      $(elem).text(text).css('background-color', style.grey)
+    }, time)
+  }
+  btn.bad = (msg, time) => {
+    if (!time) time = 500
+    $(elem).text(msg).css('background-color', style.red)
+    setTimeout(() => {
+      $(elem).text(text).css('background-color', style.grey)
+    }, time)
+  }
+  btn.setText = (text) => {
+    $(elem).text(text)
+  }
+  btn.elem = elem
+  return btn
+}
+
+// for more complex / set button state yourself 
+function Button(settings, justify) {
+  let xPlace = settings.xPlace 
+  let yPlace = settings.yPlace 
+  let width = settings.width 
+  let height = settings.height 
+  let defaultText = settings.defaultText 
+  let elem = $('<div>').addClass('button')
+    .text(defaultText)
+    .get(0)
+  if (justify) {
+    $(elem).css('justify-content', 'left').css('padding-left', '10px')
+    width -= 7
+  }
+  DT.placeField(elem, width -6, height - 6, xPlace, yPlace)
+  let btn = {}
+  btn.elem = elem 
+  btn.onClick = (fn) => {
+    $(elem).off('click')
+    $(elem).on('click', (evt) => { fn(evt) })
+  }
+  btn.setText = (text) => {
+    $(elem).text(text)
+  }
+  btn.resetText = () => {
+    $(elem).text(defaultText)
+  }
+  btn.getText = () => {
+    return $(elem).text()
+  }
+  btn.setHTML = (html) => {
+    $(elem).html(html)
+  }
+  btn.green = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.grn)
+  }
+  btn.yellow = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.ylw)
+  }
+  btn.red = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.red)
+  }
+  btn.grey = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.grey)
+  }
+  btn.remove = () => {
+    $(elem).remove()
+  }
+  return btn
+}
+
+// for ahn slider, 
+function Slider(settings){
+  if(!settings.min) settings.min = -1;
+  if(!settings.max) settings.max = 1;
+  if(!settings.step) settings.step = 0.01;
+  if(!settings.dflt) settings.dflt = 0;
+  if(!settings.title) settings.title = "slider"
+  let elem = $(`<input type="range" min=${settings.min} max=${settings.max} step=${settings.step} value=${settings.dflt}>`).addClass('inputwrap')
+    .text(settings.title)
+    .get(0)
+  DT.placeField(elem, settings.width - 100, settings.height, settings.xPlace, settings.yPlace)
+  let btn = new Button(settings.xPlace + settings.width - 80, settings.yPlace, 80, settings.height, elem.value)
+  btn.setHTML(`${settings.title}<br>${elem.value}`)
+  btn.onClick(() => {
+    elem.value = settings.dflt;
+    elem.oninput()
+  })
+  let slider = { elem: elem }
+  elem.oninput = () => { 
+    btn.setHTML(`${settings.title}<br>${elem.value}`)
+    if(slider.onChange) slider.onChange(elem.value) 
+  }
+  return slider 
+}
+
+// text blocks 
+function TextBlock(settings, justify = false) {
+  let xPlace = settings.xPlace 
+  let yPlace = settings.yPlace 
+  let width = settings.width 
+  let height = settings.height 
+  let text = settings.defaultText 
+  let elem = $('<div>').addClass('inputwrap')
+    .text(text)
+    .get(0)
+  if (justify) {
+    $(elem)
+      .css('justify-content', 'left')
+      .css('padding-left', '10px')
+      .css('padding-top', '10px')
+    width -= 7
+  }
+  DT.placeField(elem, width, height, xPlace, yPlace)
+  let blk = {}
+  blk.setText = (text) => {
+    $(elem).text(text)
+  }
+  blk.setHTML = (html) => {
+    $(elem).html(html)
+  }
+  blk.green = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.grn)
+  }
+  blk.red = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.red)
+  }
+  blk.grey = (text) => {
+    if (text) $(elem).text(text)
+    $(elem).css('background-color', style.grey)
+  }
+  return blk
+}
+
+// text inputs 
+function TextInput(xPlace, yPlace, width, height, text) {
+  let input = $('<input>').addClass('inputwrap').get(0)
+  input.value = text
+  DT.placeField(input, width, height, xPlace, yPlace)
+  input.green = () => {
+    $(input).text(text).css('background-color', style.grn)
+  }
+  input.red = () => {
+    $(input).text(text).css('background-color', style.red)
+  }
+  input.grey = () => {
+    $(input).text(text).css('background-color', style.grey)
+  }
+  input.getValue = () => {
+    return input.value
+  }
+  input.getNumber = () => {
+    let val = parseFloat(input.value)
+    if (Number.isNaN(val)) {
+      return 0
+    } else {
+      return val
+    }
+  }
+  // could do: input.getNum() returning err if bad parse (?) 
+  return input
+}
+
+export { Button, EZButton, Slider, TextInput, TextBlock, svgRenderer }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/interface/bg.ai b/system/javascript/osapjs/client/interface/bg.ai
new file mode 100644
index 0000000000000000000000000000000000000000..4ba8128d07cddd5ecf29b2242b5069bc9596b1ac
--- /dev/null
+++ b/system/javascript/osapjs/client/interface/bg.ai
@@ -0,0 +1,1396 @@
+%PDF-1.5
%����
+1 0 obj
<</Metadata 2 0 R/OCProperties<</D<</ON[22 0 R]/Order 23 0 R/RBGroups[]>>/OCGs[22 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<</Length 36578/Subtype/XML/Type/Metadata>>stream
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
+<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c148 79.164050, 2019/10/01-18:03:16        ">
+   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <rdf:Description rdf:about=""
+            xmlns:dc="http://purl.org/dc/elements/1.1/"
+            xmlns:xmp="http://ns.adobe.com/xap/1.0/"
+            xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
+            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
+            xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#"
+            xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
+            xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"
+            xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
+            xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
+            xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
+            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
+         <dc:format>application/pdf</dc:format>
+         <dc:title>
+            <rdf:Alt>
+               <rdf:li xml:lang="x-default">bg</rdf:li>
+            </rdf:Alt>
+         </dc:title>
+         <xmp:CreatorTool>Adobe Illustrator 24.1 (Windows)</xmp:CreatorTool>
+         <xmp:CreateDate>2020-05-03T20:35:29-04:00</xmp:CreateDate>
+         <xmp:ModifyDate>2020-05-03T20:35:30-04:00</xmp:ModifyDate>
+         <xmp:MetadataDate>2020-05-03T20:35:30-04:00</xmp:MetadataDate>
+         <xmp:Thumbnails>
+            <rdf:Alt>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpGImg:width>256</xmpGImg:width>
+                  <xmpGImg:height>256</xmpGImg:height>
+                  <xmpGImg:format>JPEG</xmpGImg:format>
+                  <xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A8qYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q==</xmpGImg:image>
+               </rdf:li>
+            </rdf:Alt>
+         </xmp:Thumbnails>
+         <xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
+         <xmpMM:OriginalDocumentID>uuid:65E6390686CF11DBA6E2D887CEACB407</xmpMM:OriginalDocumentID>
+         <xmpMM:DocumentID>xmp.did:fc86de9a-5403-3a4c-aaa2-c80971994cb2</xmpMM:DocumentID>
+         <xmpMM:InstanceID>uuid:62e6532d-1167-4814-9a0f-eaa1f834dde4</xmpMM:InstanceID>
+         <xmpMM:DerivedFrom rdf:parseType="Resource">
+            <stRef:instanceID>uuid:4f3edf31-c285-4e04-8055-241b58bfb4a4</stRef:instanceID>
+            <stRef:documentID>xmp.did:b3d5b4d1-9835-3b43-9b54-98c064411fb1</stRef:documentID>
+            <stRef:originalDocumentID>uuid:65E6390686CF11DBA6E2D887CEACB407</stRef:originalDocumentID>
+            <stRef:renditionClass>proof:pdf</stRef:renditionClass>
+         </xmpMM:DerivedFrom>
+         <xmpMM:History>
+            <rdf:Seq>
+               <rdf:li rdf:parseType="Resource">
+                  <stEvt:action>saved</stEvt:action>
+                  <stEvt:instanceID>xmp.iid:b3d5b4d1-9835-3b43-9b54-98c064411fb1</stEvt:instanceID>
+                  <stEvt:when>2019-03-28T14:19:19-04:00</stEvt:when>
+                  <stEvt:softwareAgent>Adobe Illustrator CC 23.0 (Windows)</stEvt:softwareAgent>
+                  <stEvt:changed>/</stEvt:changed>
+               </rdf:li>
+               <rdf:li rdf:parseType="Resource">
+                  <stEvt:action>saved</stEvt:action>
+                  <stEvt:instanceID>xmp.iid:fc86de9a-5403-3a4c-aaa2-c80971994cb2</stEvt:instanceID>
+                  <stEvt:when>2020-05-03T20:35:28-04:00</stEvt:when>
+                  <stEvt:softwareAgent>Adobe Illustrator 24.1 (Windows)</stEvt:softwareAgent>
+                  <stEvt:changed>/</stEvt:changed>
+               </rdf:li>
+            </rdf:Seq>
+         </xmpMM:History>
+         <illustrator:StartupProfile>Web</illustrator:StartupProfile>
+         <illustrator:Type>Document</illustrator:Type>
+         <illustrator:CreatorSubTool>AIRobin</illustrator:CreatorSubTool>
+         <xmpTPg:NPages>1</xmpTPg:NPages>
+         <xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
+         <xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
+         <xmpTPg:MaxPageSize rdf:parseType="Resource">
+            <stDim:w>100.000000</stDim:w>
+            <stDim:h>100.000000</stDim:h>
+            <stDim:unit>Pixels</stDim:unit>
+         </xmpTPg:MaxPageSize>
+         <xmpTPg:PlateNames>
+            <rdf:Seq>
+               <rdf:li>Cyan</rdf:li>
+               <rdf:li>Magenta</rdf:li>
+               <rdf:li>Yellow</rdf:li>
+               <rdf:li>Black</rdf:li>
+            </rdf:Seq>
+         </xmpTPg:PlateNames>
+         <xmpTPg:SwatchGroups>
+            <rdf:Seq>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Default Swatch Group</xmpG:groupName>
+                  <xmpG:groupType>0</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>White</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>Black</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Red</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Yellow</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Green</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Cyan</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Blue</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Magenta</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=193 G=39 B=45</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>193</xmpG:red>
+                           <xmpG:green>39</xmpG:green>
+                           <xmpG:blue>45</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=237 G=28 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>237</xmpG:red>
+                           <xmpG:green>28</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=241 G=90 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>241</xmpG:red>
+                           <xmpG:green>90</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=247 G=147 B=30</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>247</xmpG:red>
+                           <xmpG:green>147</xmpG:green>
+                           <xmpG:blue>30</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=251 G=176 B=59</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>251</xmpG:red>
+                           <xmpG:green>176</xmpG:green>
+                           <xmpG:blue>59</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=252 G=238 B=33</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>252</xmpG:red>
+                           <xmpG:green>238</xmpG:green>
+                           <xmpG:blue>33</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=217 G=224 B=33</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>217</xmpG:red>
+                           <xmpG:green>224</xmpG:green>
+                           <xmpG:blue>33</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=140 G=198 B=63</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>140</xmpG:red>
+                           <xmpG:green>198</xmpG:green>
+                           <xmpG:blue>63</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=57 G=181 B=74</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>57</xmpG:red>
+                           <xmpG:green>181</xmpG:green>
+                           <xmpG:blue>74</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=146 B=69</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>146</xmpG:green>
+                           <xmpG:blue>69</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=104 B=55</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>104</xmpG:green>
+                           <xmpG:blue>55</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=34 G=181 B=115</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>34</xmpG:red>
+                           <xmpG:green>181</xmpG:green>
+                           <xmpG:blue>115</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=169 B=157</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>169</xmpG:green>
+                           <xmpG:blue>157</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=41 G=171 B=226</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>41</xmpG:red>
+                           <xmpG:green>171</xmpG:green>
+                           <xmpG:blue>226</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=113 B=188</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>113</xmpG:green>
+                           <xmpG:blue>188</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=46 G=49 B=146</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>46</xmpG:red>
+                           <xmpG:green>49</xmpG:green>
+                           <xmpG:blue>146</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=27 G=20 B=100</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>27</xmpG:red>
+                           <xmpG:green>20</xmpG:green>
+                           <xmpG:blue>100</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=102 G=45 B=145</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>102</xmpG:red>
+                           <xmpG:green>45</xmpG:green>
+                           <xmpG:blue>145</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=147 G=39 B=143</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>147</xmpG:red>
+                           <xmpG:green>39</xmpG:green>
+                           <xmpG:blue>143</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=158 G=0 B=93</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>158</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>93</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=212 G=20 B=90</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>212</xmpG:red>
+                           <xmpG:green>20</xmpG:green>
+                           <xmpG:blue>90</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=237 G=30 B=121</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>237</xmpG:red>
+                           <xmpG:green>30</xmpG:green>
+                           <xmpG:blue>121</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=199 G=178 B=153</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>199</xmpG:red>
+                           <xmpG:green>178</xmpG:green>
+                           <xmpG:blue>153</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=153 G=134 B=117</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>153</xmpG:red>
+                           <xmpG:green>134</xmpG:green>
+                           <xmpG:blue>117</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=115 G=99 B=87</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>115</xmpG:red>
+                           <xmpG:green>99</xmpG:green>
+                           <xmpG:blue>87</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=83 G=71 B=65</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>83</xmpG:red>
+                           <xmpG:green>71</xmpG:green>
+                           <xmpG:blue>65</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=198 G=156 B=109</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>198</xmpG:red>
+                           <xmpG:green>156</xmpG:green>
+                           <xmpG:blue>109</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=166 G=124 B=82</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>166</xmpG:red>
+                           <xmpG:green>124</xmpG:green>
+                           <xmpG:blue>82</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=140 G=98 B=57</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>140</xmpG:red>
+                           <xmpG:green>98</xmpG:green>
+                           <xmpG:blue>57</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=117 G=76 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>117</xmpG:red>
+                           <xmpG:green>76</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=96 G=56 B=19</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>96</xmpG:red>
+                           <xmpG:green>56</xmpG:green>
+                           <xmpG:blue>19</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=66 G=33 B=11</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>66</xmpG:red>
+                           <xmpG:green>33</xmpG:green>
+                           <xmpG:blue>11</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Grays</xmpG:groupName>
+                  <xmpG:groupType>1</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=0 B=0</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=26 G=26 B=26</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>26</xmpG:red>
+                           <xmpG:green>26</xmpG:green>
+                           <xmpG:blue>26</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=51 G=51 B=51</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>51</xmpG:red>
+                           <xmpG:green>51</xmpG:green>
+                           <xmpG:blue>51</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=77 G=77 B=77</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>77</xmpG:red>
+                           <xmpG:green>77</xmpG:green>
+                           <xmpG:blue>77</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=102 G=102 B=102</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>102</xmpG:red>
+                           <xmpG:green>102</xmpG:green>
+                           <xmpG:blue>102</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=128 G=128 B=128</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>128</xmpG:red>
+                           <xmpG:green>128</xmpG:green>
+                           <xmpG:blue>128</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=153 G=153 B=153</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>153</xmpG:red>
+                           <xmpG:green>153</xmpG:green>
+                           <xmpG:blue>153</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=179 G=179 B=179</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>179</xmpG:red>
+                           <xmpG:green>179</xmpG:green>
+                           <xmpG:blue>179</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=204 G=204 B=204</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>204</xmpG:red>
+                           <xmpG:green>204</xmpG:green>
+                           <xmpG:blue>204</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=230 G=230 B=230</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>230</xmpG:red>
+                           <xmpG:green>230</xmpG:green>
+                           <xmpG:blue>230</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=242 G=242 B=242</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>242</xmpG:red>
+                           <xmpG:green>242</xmpG:green>
+                           <xmpG:blue>242</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Web Color Group</xmpG:groupName>
+                  <xmpG:groupType>1</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=63 G=169 B=245</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>63</xmpG:red>
+                           <xmpG:green>169</xmpG:green>
+                           <xmpG:blue>245</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=122 G=201 B=67</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>122</xmpG:red>
+                           <xmpG:green>201</xmpG:green>
+                           <xmpG:blue>67</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=255 G=147 B=30</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>147</xmpG:green>
+                           <xmpG:blue>30</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=255 G=29 B=37</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>29</xmpG:green>
+                           <xmpG:blue>37</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=255 G=123 B=172</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>123</xmpG:green>
+                           <xmpG:blue>172</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=189 G=204 B=212</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>189</xmpG:red>
+                           <xmpG:green>204</xmpG:green>
+                           <xmpG:blue>212</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+            </rdf:Seq>
+         </xmpTPg:SwatchGroups>
+         <pdf:Producer>Adobe PDF library 15.00</pdf:Producer>
+      </rdf:Description>
+   </rdf:RDF>
+</x:xmpmeta>
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                           
+<?xpacket end="w"?>
+endstream
endobj
3 0 obj
<</Count 1/Kids[5 0 R]/Type/Pages>>
endobj
5 0 obj
<</ArtBox[0.0 95.0 5.0 100.0]/BleedBox[0.0 0.0 100.0 100.0]/Contents 24 0 R/CropBox[0.0 0.0 100.0 100.0]/LastModified(D:20200503203529-04'00')/MediaBox[0.0 0.0 100.0 100.0]/Parent 3 0 R/PieceInfo<</Illustrator 7 0 R>>/Resources<</ColorSpace<</CS0 25 0 R>>/ExtGState<</GS0 26 0 R>>/Properties<</MC0 22 0 R>>>>/Thumb 27 0 R/TrimBox[0.0 0.0 100.0 100.0]/Type/Page>>
endobj
24 0 obj
<</Filter/FlateDecode/Length 95>>stream
+H�$�1
+�0C��\�6�v��W���G(꤃�?�U���%�Q!^W�6��^�gSG���,"�#�Mv�9�X#�SrF���'S��G��{��
+endstream
endobj
27 0 obj
<</BitsPerComponent 8/ColorSpace 28 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 12/Length 21/Width 12>>stream
+8;Ufc5.CSXJ,oYlLNj^~>
+endstream
endobj
28 0 obj
[/Indexed/DeviceRGB 255 29 0 R]
endobj
29 0 obj
<</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
+8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
+b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
+E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
+6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
+VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
+PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
+l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
+endstream
endobj
22 0 obj
<</Intent 30 0 R/Name(Layer 1)/Type/OCG/Usage 31 0 R>>
endobj
30 0 obj
[/View/Design]
endobj
31 0 obj
<</CreatorInfo<</Creator(Adobe Illustrator 24.1)/Subtype/Artwork>>>>
endobj
26 0 obj
<</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>>
endobj
25 0 obj
[/ICCBased 32 0 R]
endobj
32 0 obj
<</Filter/FlateDecode/Length 2574/N 3>>stream
+H���yTSw�oɞ����c
[���5la�QIBH�ADED���2�mtFOE�.�c��}���0��8�׎�8G�Ng�����9�w���߽�����'����0��֠�J��b�	�
+ �2y�.-;!���K�Z�	���^�i�"L��0���-��
�@8(��r�;q��7�L��y��&�Q��q�4�j���|�9��
+�V��)g�B�0�i�W��8#�8wթ��8_�٥ʨQ����Q�j@�&�A)/��g�>'K����t�;\��
ӥ$պF�ZUn����(4T�%)뫔�0C&�����Z��i���8��bx��E���B�;�����P���ӓ̹�A�om?�W=
+�x������-������[����0����}��y)7ta�����>j���T�7���@���tܛ�`q�2��ʀ��&���6�Z�L�Ą?�_��yxg)˔z���çL�U���*�u�Sk�Se�O4?׸�c����.�������R�
߁��-��2�5������	��S�>ӣV����d�`r��n~��Y�&�+`��;�A4�� ���A9��=�-�t��l�`;��~p����	�Gp|	��[`L��`<� "A�YA�+��Cb(��R�,��*�T�2B-�
+�ꇆ��n���Q�t�}MA�0�al������S�x	��k�&�^���>�0|>_�'��,�G!"F$H:R��!z��F�Qd?r9�\A&�G���rQ��h������E��]�a�4z�Bg�����E#H	�*B=��0H�I��p�p�0MxJ$�D1��D, V���ĭ����KĻ�Y�dE�"E��I2���E�B�G��t�4MzN�����r!YK� ���?%_&�#���(��0J:EAi��Q�(�()ӔWT6U@���P+���!�~��m���D�e�Դ�!��h�Ӧh/��']B/����ҏӿ�?a0n�hF!��X���8����܌k�c&5S�����6�l��Ia�2c�K�M�A�!�E�#��ƒ�d�V��(�k��e���l
����}�}�C�q�9
+N'��)�].�u�J�r�
+��w�G�	xR^���[�oƜch�g�`>b���$���*~� �:����E���b��~���,m,�-��ݖ,�Y��¬�*�6X�[ݱF�=�3�뭷Y��~dó	���t���i�z�f�6�~`{�v���.�Ng����#{�}�}��������j������c1X6���fm���;'_9	�r�:�8�q�:��˜�O:ϸ8������u��Jq���nv=���M����m����R 4	�
+n�3ܣ�k�Gݯz=��[=��=�<�=G</z�^�^j��^��	ޡ�Z�Q�B�0FX'�+������t���<�u�-���{���_�_�ߘ�-G�,�}���/���Hh8�m�W�2p[����AiA��N�#8$X�?�A�KHI�{!7�<q��W�y(!46�-���a�a���a�W��	��@�@�`l���YĎ��H,�$����(�(Y�h�7��ъ���b<b*b��<�����~�L&Y&9��%�u�M�s�s��NpJP%�M�IJlN<�DHJIڐtCj'�KwKg�C��%�N��d��|�ꙪO=��%�mL���u�v�x:H��oL��!Ȩ��C&13#s$�/Y����������=�Osbs�rn��sO�1��v�=ˏ��ϟ\�h٢���#��¼����oZ<]T�Ut}�`IÒsK��V-���Y,+>TB(�/�S�,]6*�-���W:#��7�*���e��^YDY�}U�j��AyT�`�#�D=���"�b{ų���+�ʯ:�!kJ4G�m��t�}uC�%���K7YV��fF���Y�.�=b��?S��ƕƩ�Ⱥ����y���
چ���k�5%4��m�7�lqlio�Z�lG+�Z�z�͹��mzy��]�����?u�u�w|�"űN���wW&���e֥ﺱ*|����j��5k��yݭ���ǯg��^y�kEk�����l�D_p߶������7Dm����o꿻1m��l�{��Mś�
n�L�l�<9��O��[����$�����h�՛B��������d�Ҟ@��������i�ءG���&����v��V�ǥ8��������n��R�ĩ7�������u��\�ЭD���-��������u��`�ֲK�³8���%�������y��h��Y�ѹJ�º;���.���!������
+�����z���p���g���_���X���Q���K���F���Aǿ�=ȼ�:ɹ�8ʷ�6˶�5̵�5͵�6ζ�7ϸ�9к�<Ѿ�?���D���I���N���U���\���d���l���v��ۀ�܊�ݖ�ޢ�)߯�6��D���S���c���s����
����2��F���[���p������(��@���X���r������4���P���m��������8���W���w����)���K���m�������
+endstream
endobj
7 0 obj
<</LastModified(D:20200503203529-04'00')/Private 16 0 R>>
endobj
16 0 obj
<</AIMetaData 17 0 R/AIPrivateData1 18 0 R/AIPrivateData2 19 0 R/AIPrivateData3 20 0 R/ContainerVersion 11/CreatorVersion 24/NumBlock 3/RoundtripStreamType 2/RoundtripVersion 24>>
endobj
17 0 obj
<</Length 1084>>stream
+%!PS-Adobe-3.0 
+%%Creator: Adobe Illustrator(R) 24.0
+%%AI8_CreatorVersion: 24.1.1
+%%For: (Jake) ()
+%%Title: (bg.ai)
+%%CreationDate: 5/3/2020 8:35 PM
+%%Canvassize: 16383
+%%BoundingBox: 0 -5 5 0
+%%HiResBoundingBox: 0 -5 5 0
+%%DocumentProcessColors: Cyan Magenta Yellow Black
+%AI5_FileFormat 14.0
+%AI12_BuildNumber: 376
+%AI3_ColorUsage: Color
+%AI7_ImageSettings: 0
+%%RGBProcessColor: 0 0 0 ([Registration])
+%AI3_Cropmarks: 0 -100 100 0
+%AI3_TemplateBox: 50.5 -50.5 50.5 -50.5
+%AI3_TileBox: -244.480000495911 -434.239990234375 344.239990234375 334.480000495911
+%AI3_DocumentPreview: None
+%AI5_ArtSize: 14400 14400
+%AI5_RulerUnits: 6
+%AI9_ColorModel: 1
+%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
+%AI5_TargetResolution: 800
+%AI5_NumLayers: 1
+%AI9_OpenToView: -38 19 7.83333333333333 3238 1903 18 0 0 89 170 0 0 0 1 1 0 1 1 0 1
+%AI5_OpenViewLayers: 7
+%%PageOrigin:-350 -350
+%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142
+%AI9_Flatten: 1
+%AI12_CMSettings: 00.MS
+%%EndComments
+
+endstream
endobj
18 0 obj
<</Length 65536>>stream
+%AI24_ZStandard_Data(�/��X���E})И&�ri-�ϖl�l�����M~.�ED�t5%�i������4a��QI$��T[��^�y���(����W�,X�c#��9��å�h��uV��AM���闭�j��4첗�i$?���.K���ǫ$�ż�W!Iam$�Q��&�y��G�M�X9��?�Gn��j��wQ���2Q^=S�]B��۸ۣ�>ˆ��ٰ��o'I�G�.h㖥������6�N���/I�03W};i��%�M�����m�,M�ii�����+.���p@�<��.A&hp���$00� Aq��p@ `�@$@���a1A��.�!���`X��1��������`�l�uA�<H���W�V�t����n�H�ݕ$��; �`�
X�AB��D�p����C�j�	���bq�b�x0	6�v�(��/�f�v����}�Аҿ��f���Ȍ������<N�t-�u�'ͨlh7�Jy`��A-���w�҈Q5��w�ln|��1�J�0rJ"V�M͝Mf`�K�u��L�"�8.$��
+�S��^H,���s@$����a����3���,ם/��H,X�ch����t���>��RP�=�����6Q���8�U(8�
+
+�
+d
+cM}a�/؜��n�;4d����.p�S	Z�fAqu@���4����MV��@j~�8ΛL��i�VU��eVMfP�˚Vy��R�/>�����JxFe��d��*]m�Lf�����T����ц��i�z.Ϲ��,'^)Xa�@�)hq�X��O
+v@,��y�O�UY%z�SEBCģ.�$=rN���/|NT�gHTA
+S���b�������U�S�(�F��eD�LK2��n��j~Sdba�H[��w,�Z���bA
+"J9 \t�Dh��md�<n�G�yѪs�񳧹�^SYuVt�ǻ��5����F7�k��#Q�O39ƣ�E.,$R�lD�I��l#Ş���޸a'͏�uܘ���a{f�C�3,j���W;�u�n���V6���;
+��WXT���P; ̴�"�PW(
+=�܂��XX�-�+3,4��\��#��p�"�pV�Qr< ��/kƐ��*�aT��q@<�8+����	�< ����'�eXPd��8 ��x��),-mcD"��p�W��J
+h303s@l�@$O�ʟJg]��h�AgYE��`��,*��x@,U���A�}
+�0
+; �1��8�!u�L���O��<���􀠲���rq@�TtuW���͇W��+�b��@�`��X(Pvx@��������< �žX,�a>�X0�z����P���PX�YX(��y��!�M�D���H#[H�-�B)T�xe��p; n����P8 ? �"q�-,��LQx@�yh!���ъ��H
d�
+�L~�ᑹ��|YO�1�+���m���;2�Nf�ݖww�!Yfد|�Fv*̫��e%y�����4��[Y�KqR�o2�4Dt���:vd��;cDcTEh7�+�4���w�9 <�8 ��Ae�O(�1]�&;< %S�q���WSS����Ba���8 ,oq@�%�0q@((���7mAa�HX�u��Ba���H��
+�.~@|la�< ά|e�p@,�3/(�S�1�6�p
+�������JQ�.��‚���,Ҫ�DZ�M0���c��++̀
+_8)�ƌb�,0���\��1~'s@���f��t4&���H����B��
P �bE}��R(���eF�ʌT��94�e{?)q��U��O�i�7J!4ˌ��9�fY�Ubf�ܱ=82���r���)wx��
+}yVWfd�`�̰ön#gZ��5",�+3��Ȯ>Gx�+;i��q�d�8 .�� ����%����XX�"��x"C�ZP,((
+E�"Qq�S��e�gቼ�%�N4q@�aq�+$���S?
+�x�AI��.(0��D4,,��[���;��:ZP�Ҭl�Q���+����eR���b�H�˰X,�'�P�'���p@,��xxv@0,,�'.XDy@(�¢D���chQ��!%%�N���XL1P�EEUY��b�FFDY��Y&L�sֲ�����ba�T�"�= R���p�D*P*���P��aa|l���<����Xp���.D�"��0y���; �	�Ba��b��qy��İXX�œ(�S��C1�B��h�0CMX����G
+�"A�*�B��a!�������bAa�Xx���ŀg(�L��f��BC�6�l>��w�
++x�3x�d"*2:B: �N	R�-h�h�墪����z%JQ
+���b�������-�0�)�����������/�8�)�����F�B�C��c��\@�l:)+-/1��g"���\H���>�n����+|�3|M8A�JX������"��X$�bUXA�R���H�	�B��X(�Q��H�,���PXP`PZ�������/�Ba�X,,��/�B-��-�B.|!��2��
+���(LMe��
+*`>�P��0���b|��b�S,(��T�p@�a>�8 �����d8��Ba�0��!��`8C2 �1�1������p@,_�B.�.�B-x�����B��"���-lQ]̂Aa����PH(�*��)�Ułb�PP$W��()�Cb!�H($	E"�x"��D%(��$�p�+����:�������Td"�������$.PxLD<,4nq
+���%.Ϯ�כ; la
+�`	�������j��P�JT�UUkj�� �������P8qf���j�j2
1,,�p�H8�|�ъ�.
+�D���gU��a��Y(Fq@$&1�IL����9��5FC�.��P$�D�����e�1â]��XT�
+E��O< �W����9 fa���Q��"q��9 �ȝB��0�s@x���b��8 0�,0"< ���	@�
A1�0@",\`<��.���4pP�6���d��<H�l�0h��
4H�`��
+D������	.0BN��� ��C�!��A$��H��$H�� �(�@1 ��40p�� D�
��H��!#�&Lp0���	6��&<����
+L���Aƃ@����AT�0x��h A4p���N���`` 48��C�c���!x�H���@8 $� �N�	0 ����
+\�8 F„4�@	��;
+&<��$` A�
+D�0�L�� �$(� �B�p@\@2#��&Lp��w5���?~;#9�!��.@#�12�"@
(�`*����4��	(���gv��xG�4 ��|
��A2@�r
+0���Lx0
`�0`�@�B A�`���(��L�&h���D�0���<���T4���,Ee���^33YRe`����d��� �������&��A'����>A#x``��6A&<���8���@��8� �!
h���`!��
+�HtLp��`���s@"��	`�*��:0L��`$0"8`��@�H�@��<h��A&Dx�
+$,6x`
6��cV A1�B�AIׁ�-���R��*0��c<>��Lp�� �� 8��@�0���1��<0��` @	�Q��"H���4pA: ��#�1�@	h0a`���@�&8x��B�p@�2�HA�����9�z:KJg�c��?��%����<��/��PUZ�/)�z:����*ɥ�5ڍ�~gÚĎ���N�)�*���E����p$���W�4K5͡�;�
�����^�2��3|a��CuIT�_e�|X�u�C���r/��X���z��`muD�\������F-�I_=��zW���FV���
6��{�x�TDfv�?�d�N�E��q�Ӻ��yw5�h��Y֊���eJO�/;[�f�ފ�����5f�YvW��io�N�)��K���ь�j��g�lghx�4�d���u=#��-�vE���B�˹+;"��j��fY~���%99}�MW�^��FVU�+%[�6��g$Α�=�524�t��62S����'�@nR��%�͈l����˩�wC�s2�������n���
��������:;W棓ѫDRW;�KOhG�������g��T����"��p�s�+��&�J�^G���ԓl%��g���;+fY��#�]ևl�����G�w���
+���4���Үl�ӟ"3����;�)Ӑ����ʺ��t�NUC��,��յ����]��j�)����}����k�
���l���<��.�t����Ç4�Y��-^�N�]N]7_��?��e�}��pXu��d�V��ZyuI�j
��ij�	/��Lfi3YN��)�d���}?�2�2�[��=��M�Q�u*g�Nlje�N�o����z�,�IGx�g���!j��vH�cY媇tU�=
wr�-��'��6R.��L!��$���̞t!ay�Ou���l�w��Ic������|��$˾WC��xG;�++��PJ�F����žJI��齱�+B�;fIL
��h�7yFv;�f�S�WN�:���x8v/�I�*��wt���o��ʲ�;�ut{�����g��R^[���i�\u��ɴ��,�nK��T򠕌��
+QG�
ޕZ
n����3������,]��]�Z&�+~�S{��PC�O�q����U�ͪ���Itc/k��uH}�Ɖx5uU����z�R�Ɠ�>�'e=����	�nƔWl\Ӥ�є���OnL8�YO�#�����ˆ�n��8Ş��%�ê&���������>D��[y�4�{/S\-D�Z�ګ��˩Z�#++g���J!l"͕�fP�M��>��RK��X�^t�e�d�a��6�6�������"��u��v��صvJ�����۲W���+�s����K�k4��g&�^��$��������[,��~'�6t�r�3'���
7h�є�a}����7�<K��;�-ub���+
6xzT��$/Oxw�o�GOl��=372�9��ǩ�.TxGG?��Ɂc%Ϫd�kz�I}��>t|I=:�d�*�f�-�M�W꒯�t�����0��>ޕ�Tg�S��v�^���i�
���-��Ҷ<��^n�=�eK����^y���j�A3A�[Gv�P6�N�,!��N�#�K��B4<�c�	Z�)���ܽ����#u�l͒�ɓ�L��̏H��R�Erw^��5&>������$���l|&�|Z���J����4���M�~�h��*������y,:���䭽*�>hr���+���{Etޯ��\+��
��f��F'$���Dw��T�ܺS͈��)��j�{�,�&���f��
��X�:��ws��}ķ3����T�Όgw���ax'Y�x�#�H{k��j��i�T���ⷵ���������4Yy'yc�������4�=g.�>�R��+��6�W9�]Y<�JJ7�8��u���i�짜�Ґ�����D��uЙ��5,���v�4�k�|�j#{V^(;�Lҏ7�j�����Ɖ�&J��^(��:�+8iM+�eOf:�U_ݦ�Nd������+��p&��U�_�qʌo��/؎ˣ��;��V��j��H�&+3�5[v+�4k|�������aGߛ�]�s�$�LO���r�^�eΆ��d"�4;���6yv���qL���s�QuX�6w���vb�d�ݪPn���NG[���	oف㹴a
+]�Ǣ��1���K����䝍h|Lu֛ʯ�*��W)�M���4K��fL��߫���^~#�ʬb�ǣ��b�nU|Ge�Z6#��&�uG��|��
�[��J��]��Z�<�a�:�ʬd;�4bv�#yO���%q���44'�>Y�����:��M�r�{�zQ�[S�Is ��n6(�F_)g��X�w��F�\�.�ڔ�yA{��3�n�&i�Vތ[b�/�+�Z��b� ��M&��Tr6��M��n�%�R�5�:b]\?G�?�g�NG��T3Y�r��G�-�a�U�k6R�a:Km�i�z�O�����a�2�[���J��֭�g�M����{Yfk�feQ�@iƣ���^w��RIZ�ݩ{���:h:RI��F�H�b�P��yb�L����֫�t+o�n����Y���>A��ݛk���Nt%�t���4ɧ7$�=�(/��Y�S{�t����	�pm�ݔ$d�&��� �-�Ɏ��s�=g��f�<����z9b�����5H\�<8�\��
+ 	��.����.��� �D�*�h�↪�q�<?��+�g|��\i`Υp\]��,�`�|����
I��tB6�^b
�f�^S&s6�H�ē�lET��7��L��ord.�����Jɳ��F��IM��t%����ɰgIVT���3�×`����9y�&�u��q�T�D�/ҡAS���߆el�܍s����$z���n;Ctr��%�#�j�^�
+I�8�a�i���q6(u�让�4���%�٨z�RJ�'V#m�SE=��q��+9򠢊���]NO’)��z<�=��)�֞��N�le:�G3��#A�Mj`���II�Uj�q�Tձ+���V�IGֳ�|؋f�Q/wW����Noɝ�'�b��{����3k�����OK��5�g��ڹ*�w�3.~��%�m�y՛���Z~~)����/o�g8sv�eY4h7xk���r�����kM���)�
VunwO��J�Cx��OJ2��V�᝴�Z����4�4l
��������u�R��ظ~Y#CF���n
+���s˾�I�ޜh�XɅ�K���o��S�����L�ea�:����;I�^��Xx�3u*���-��l�0�5�B~OtK�xJ��Nf�͉�>�w�bU3��$m�99I4I����rl�h&�ʄ�7���I�����X�wv�
+�L��O�Cې*�M4z��آ�*�#T$":W�kh7U
+Z��	;�wE57_Mj�s6wX9̞=�p<g��k$;:��s]���Lu-4��`G���F&X��������2m��}�P�.�Q8T���վ�1�}nDteoy
+�Cw�R;��WBd�����=).*����X=r��G���0J��M���:	ҍSe��t-�d���� 
J42��g?�H{d�'i5Hi�
�K�3�Q����(]�2��>c���f����\-X�^\��Ci������|�DC�2i���U�/�����<��K�ê啛52���J{�z/Ò��v-Ig^"I:�=��X���F�|��U�F��σ�hE3L��Q���a��m���W
��=�;D�3��-��ݫ�i����>�1G��N�3��Tody��]N���iGG��rw�|j]�n¾�K�fkw;�$�^�CkֲDZK�V�]R;6&�w��0���\�oe��;��&{�|�i�W�k�ӓ�5e�Δ�i7��6��o���7��2S|.�4_�ȋ�iyI�џFyK�)�ni�����K#)��q����a���n>�pS�6I�XM�F�g^��ijͷӤ���ء�Ɇ>R���tt�K9�ϝ�Lp���KѠ׽�jB#�a4U=[�2ir*%�s�ҊH�E��J}�GZ�e��:eiE��,��þ��J�Òz�c��zڠ�CC����21Z�d�֖�^'E�1?�R^%�ԩ���|ǚKV��ާ�#t����оW�+�nξOO���Q�J�3���䙴;������r`� Z(��o��7J�i��4GC��P�F//��h�b
�M���j�kv�#X��I5&����*i�qӢK1ވq�ӎ�U�J(��a|�#;���Ix�cٴ�U��=�k&-t��K�O	�x����R���]ľǑs�wȮu��q7>�ѬrtTce�x��W�S���h=��)�g���UɧT���I�x��	%�Ng�vfs�U�Ґ$������Q�X��IbYY�J��Ӳ:�9͓H�#q��|�%�ܸe�G�j�ߎ��>�jp�=�ث^�ܑ�^6���b�Dn�,����g\�̸*�a��Z����e�����I��we.mY$+ۭƒ��';3��Lf6CI��m��%gΖ�<����,2㍌�sgt��t���jC�ZeUQ�lZv�4���c;j�䒛)���x{j�XU<'�٩BW��C��2��?3�v&��]s����(3u%��)�dZ�Ns%5��۝�Y�-�-�xo��8�I�#$*a��h���H�4C�騥���姪&ug��{}��j�.O���m�}U���Wf�i
��x{��I�(�NG؛�:ÒԚ�:t٘��ؙ>�z�v��n��Y�j���е�e�f����X;��}EA*����	ѽ(M�ʑ4����rql�1� U��4%�imKOa��\,*��w+��p��8i���8������وQ�0�Ψ��i�aMN�z.�,:f/�Ū̚����g>�<d�b�R���0A"��'��pA*�`�4`� �N �`4�x5v'I	&���l$(\0!ƒ
T�� ,<���@��	x�L���	p�Q
'�KcOzL��*5�|�x�Z>��&��y�T�Œ���&e���!u�S4�Х4RT9�{���,�$I�
�O+�A{5����P�m?���5���K���2�$��VIU��D)�|f�&e1ʼ�Q��M�|W1���f��Ӓx˱H&�(Mg��\rڻ�inoS'7���Vx�)��T�w���Qͥ
+�����~�]:Ie��BHhi��	��eJ�R
��4Y�
hd��|"����HGbT�˛��$�*I�˚g���41�eٙ�9xdt�Yo������h���Sc�v�Sn8ɚ��:���)��y���\s�F45yg��{-��W��ID$c�݈J���jJMGe'��:��/����̿O���aaCbK�Lג�&�I�+e6yv��*�U%n@�2%�~�����|�~g����o�Xr
�e:��`��Y��{Z�R�Qe����^.�y�6ܬ��*�v�*�}4�;�Y(���q5�Ul�5ʐ�h��Z�ye�k��}|[M[�����q.lv��R?C��K���5�E��~/"�A�����]9�
hOe>Z����\�7��T/�����?t{���v;��ec�5���15��Jn���"��n�ԤdC�,B1rC6ohk(���d���&y��x�g��z��M��8GRؐAC�Vٴ��
d�G0�4;�
+e�J�t��F��ɜ�BK�UK��y�*�5�f��6_�|���B憴Z]iߦ�֍�R2���$�%!�ͦ3s����9!������S�N^����3�d�!4a��F:�G
+�J��F�ץ�*��D.�m���bo�s{��^=��կ��jۍ�T��He�!��:��P��
�4��\V5u�B1:�ˤ;g,�%��p^���19�����H�7�d���p�l�s�k���'��u���m����]ҧ�!�笪j���槬�g��IZ{�DWf$g��Zd{J�3?�ɛ˭㠉���;Z��$ym8ev�㱽�'���T�o�����\��}�:c}>$i��+��h�'S'�Q'�fx�hx��,Ӊ>+a�[t7�>����Li���/���b��M�����-�O������ͺ�L���}�'�wg%�!�E�W5���C���G<4�>��M�r�ʪI�u>�N�l�i^7+��m�q�慃�Gs��;�l*�6��S$��xy��<c��k�]�]�2ݣ�~(���zS͆Y��|�����5�9�FF�3ʎm�9��ڼ�>�UK&�YU�a��D��Y�Q�3����Ǟc��OҴ�ߦ�9��:d������I�B�
]�Y�.*ĺ5˯ݰZ�~ߎ�!�ݰ��fF�!�dPwr�8N��yG��s��GE��*����L��G�����;�U�/X�N۝!J��i�8Q>F��(�]y&<ħ��;U6x(�%���n�6[L�׳���Jȓ%Ik�<_�~v!���,t
+!Y��Hwe*�:���ݵ��N��;2��Q=�ܝl��v���UQ�]Rɧ��yWuEtw�
}G��f���
+ˆ.͈ܡ+K��%V�����
_�~)s�E-9
|���JF�#���3$��h{y��z�[�֫}�GhRg��w+���4��� U-�2�QU�D��t��2]���4��c��<�8T6)�.���t٤˞Ib*����΢L�è��&��R�v�P�Y�������-�Ī0�.�t�Μ!��h�R�'c��L4�]�d�ݯu%��f�� �3^�d���W6�k]f�r����Ll��чoZ��}WSt������K���+#.5�^45�a�2�Fh�
C��e�:��>��|�I=yg�W�6�;�]�
+�q��G}O�{M�4���>��;�`��m3Ȏ3f��мQ�[R�y%���1oy���C4B�����dWObU�",��a�5S����ό6�?���Z/�ehZEW�)�D�֧չZ�Z�j�x��;�#��U�CJ�)zʈ�s��Z�)��l�2x'��N�{�i&ڝ�ģJMY�����h���� z�F4CsTWF�$"���:V�_/�����
+I�>Y������\�YJǦ1$�L�Pa��Ќ��'s�D>�D�[�r\Q��Dt���|�)�I�(�s?�Yh��[8�[��\xjh3�7�J��ɑ�*$�l�)�0�,�5��ޤTR՜��죤_ߛr���b,;1�(/ϑ�fi���/�y~��i爬:Gd�I��nD�V��,	1�6r�@;�qLB�7���ч\H�;{�s�\鷎R4׋ov���r�ȕSE�S9U397(�9U47����E�Qf3UG�I�r:d6�n�.���
_͈�M>߰��4;�:v�Xu�>Y���r��sB�9�j��bUa���xigN�4�t�Zy�X�,����خ�+�4����4���*
+�]�޻O����U�o~f�&���R�
�;wrS+$�����.��E�O�X76��srl2ma�4)���㢫)��L���s-D��A�	&H�@(hhH(#��A�	8��� �4�Ԝ37�Ա8�d��a��~ͦĶ\�	���Td�]�lGu�����G�����Ҫ����Ŗu_�jF�Ms��>�d�ɫ�����z�lͲqe�lLx�fb��v�l��?�/s�T���,�v׻3���]M�����>���쒬���4Lk�t�y��z�/}U4����)\�R��W���Օ�
���>F%hg5Wsg��3������r�M����,m�訙��<e���h�r4jjidĬ3<J
��ъ(/ś�����OUi��)B��A6*#�)"�=]Cy��b�g��*�*Ki�T��ܗ�@#�T����ƮFRC��T��'h���:J��1^_>�,�)a�d�R����s����O��A6���8뫫��hV�w	O�t�g�>�USCo���h ᵒ�rʱU�XD6N�~ae��2+NM�IV�
N
ɤȶ����(�b���,&]��D���4���l
+i�3%3�Vg(i�3EX+T0��H�T5��mS2UE��+��F!OQ:��:
+ٖ�(���̯�aʌM�4�yk[IB']�yijj�X�*AB��}
M����^ǴɌ
�j���z�Lˢ��kE�o�yg��а��F�	�
��gk���᯵K3Kv�?�����X(O&6��T63�geuˑ^�e����0⠥�Ըa
�v�F��K��^h�t=�L�Q����+�̔긤V�n����X�ht�ǹ���lp��gԴ�?�;��0���QO\��lա1b+	ˎ6�cm�HyuGD�U�	%k?�����,�液PۼB�*�lN�2_#�l�I����hy
�)�e��Ȗ��8����Th)U���Δ�,G�}&m�#��pn���`e�Fo�{wd1��a�&�=����K:3��Zw����t���86��27p�&'}�d퍮���S���Ɍ�\X9&K��)�����YGf��&ޮ�&w���9+��mV���@N�"#J�Kg"�C�!+R%U��(n��6iBGV<�'�"��f^��{�����^��ڬ�iHXGW���X�O_e�j��w˩獯�>UՉGc�����sW�D�#�������ʴ�c�L��j�k��|�N�4*�kn�r���0ov&�YUj.�V��6w�#,|N�ѯ�b����y��NG)f�OF�=���U�_��Hgy�9+����i���^2L��׫�I#�A-:k�BW��:I�ЇM�L/��G��sA��Dvґ��w��nRB����F���^r�O�\4ؔ2�p�]s��f|��&tW�L�b.��2�J.��T�ı����k�؏��:Ib-�<ƫR�a�?��x5-4�0���:ks�j:���ޙE�C�O�ľ��Pً'���>5+ѫ�9�{��(�A�x�>?J��g�0G<� z��� �Aj�>X53$d8WB�QӘc/�Z�]�K<�1����c����jo�������\}ud,׵����������.+e�L�S)��H���� 88E�A�^~���FV<F9�'jNc������`��u�y`
+ٻ�6�<|K���Gi�ػ�&#�IM�yH��������ɱ���]OQ�8O�Rm�������59���ԃ���
+F��I9�*�7�[5��>�!���Sr/��Zㅲ�����a4����rq�gMzX恺�Q�)o�C�8�>�nt?��Ik��$ӉxHV��>@��)cP�ժ���B���+���u�-����=�2�]~��)z�*R2�PF��Oz0-�vH��	�������{#D��@K��a�3t�3�!��ə�{'bI�Fs2��bo��ӫ-WW���u� ���5M6jZ���dG�i�N�@؆@�ˬ��iGe�{�ie#�tP���Ve�j%�ˬ/a0�uae7�f��^~��,iR���v!����m�ѥ��P�g|I1���2�-��A�o��7EN����,��1Jv�����qj�	��	��X��kZ�gmB`�SϚJ��D�%���hu�6�`���&�q�D�u�[(��XcdD��4���8�T
ʶ�\��w�bB�i�0��qgd�~�C� �"�����4)g*�}�N�6�6e��k�֯���]a�X
�;
V8�'Ta����̎���>�~�Q�-qn,�
+=&�=EB`DJk�^�-�S(��Ρ_�5�ϙ��'��x���皘pÝ�33$n���'ƺ�{���v��Sdg�șE�'��Q �H��\�5�T�d���uyIR!]EKS�����9�&��HVA|��ޯ�KW'4���`W�s�l7�-�W�	��i�@x�I[�:8�V8�x��^d�Qf�K0F�
+�bϷ�)E�G�ڄ�RO��3���ijT�M�l�uB��`�=@�1��
 5ߊL��������З��Q[|��:�ֲ���6޷��A�`"�A���Ʒ��Iv����7ӳ�{�ۂ���U����ޑ��N7d����z£�&v�*Y0����P�9���2���Æh�������M��lM13[�^idžL:��],��"pڂ��񽠗��Q9�/���8��;z
+�H�D�s���(ከȩ	a��6,�w��n�܁ܬ
+y񶛼��B�š��-Ǐ����[��8߅W���q�UF3{�B3�K؄�ij\��`�Y���q��]��a�vj�AڀZoJ�du��E�l�
'(Ɋtd/~�9*��Gs����pn՜���h`�F&!Edy�MqQ�-�:(A�T��děs~F�	�QJ��K���7��he�_�F��y��щ{M�W'0�ݶ��S�
+��h����S�R�	���sB���%���p���,rh-"4���ڭ�
���id:��WIů��v�4�����aJ�loi�����ŠuA��`\�E��ƒ��A^1��Y��n3z���h��#X�Mè��j��ۼ����ڂA��3���j33񿒴���:����c.�p����7�$X�i*���u�ih�@��b�:�&�/ej�$�Ι��-�*ڮf�\|����`@�Y��}����2[i����L��}���
+�w'Q8P!.p'Q�D�~.�ݞY�G�k���+��޳���	˴Bi�:���l�v�9̚�c�J]��䖑��h��Z"��Ǔ�7�[}0V��i��aT�/rC�`M��,�A�V$
��B��hT^�u��S��[�؆�P���}����2�qg':���@�?&��*�`�v�h�V��Vbi�1�9���,~섢�q3u�]15������d� �|r����|Zz,X���X����D@��n�)�ȄBQ�gX�{�����A�H��˹1g7 ��ڽ�VV�
+B�	�^Nϲ��:D�)<V�����oC�0�g���i�n \�t�t�N
+U,�_�'=� 
+ʡ!#z��j��0�H�?�=";�>���������V��JO���G����GC�p�1�gr�">��0Ҳ�?E��R2X���cU�D>@�6S���0��+�ov�"*"��_u^����qcQ�ҡs�*�0j@�C�+h�}����� q/D�@�r�Mpx�$u�X���Dj����W?��lIV��xD���P+�1���<#_4z�n�JN���POvȤ�f�
+:c�+y�^��#}~�~l�+D8k�-m	���d��B��?��0N817{q��$���šw���P����G5_^a���L�bBpk�V2�������l�ٞC�#��]���
��1��R��ĐT����+����!x����x��������P,����3�{�L��tN6��܆8�j�yQ�Ǹ%>�,:���L`5����o�����7V `��+��FRp"����
+(�8�y�"�ی�j�t��M���H%.�V�=&���h��UM�Xh�E(��}�e��<�>�������We�0!�~ä�@F�{�'��r3G���S�B����l��?/,�k�A�
+��V{�L7�%'��Rc���I�y�5D
�{�ZT�*�<�{��kU�p��w}O#���놏�n�N�f�zS^[��K�5:�Z,e�2H��n+�r��i$+��	����w�F�M3�#�m�PRUP�=�xa���Q��h�g�ekf����uIBv��pz�'QjK�[��{���z�0�Sm��Gk{�*�����R,-�D���,�0m�m���Q�t��48���qI ���!�+�*�|��u�h|��js��38F����Ǯ0�(�F���gi�0"��	��Gb��f��Z�u�� {R�!�b&��?�����$�BD��%o��w	��6��ۛL�qA���S\��oq�ߒ�'�3y
+���\���
+ΤT<�ay�F�����!BL�9g�4Ʌ�"C�L�#�\���
�$�n^�|%T�LLtA�2EɃ�h��&)�&)����6�������K��DƁ��8d��:�t|�y;����]���yM�����{��'JgNg+	�5�����K��i��c��O`�1�*�	p�9�6V�8���������8����&d��\kD��|5��E��5��`�
K�trp*�"Ӽ�1�6�_Ǵ���$"b�4*���6w��� ��s�p�)��|*�>k�[xn9�\Y��-���Ͻ�}E�,�
B�6fW�yFs���Z��'Έ?
8t��]U�I5���(%��������a�.Yq��Z�D��,�1�h
+8�d�8�0̑{N�[�RǴ��$j ������d�$�ܸ�<ğ�y�(;��ʞK�G
+#8#V	��y�x�w"|m3Ҵ��)������ݲ+���IŅP$���ӆ[[�V�a�]�� ��HG�36�7��hc�x�p5|K��9j�MN*���(���pђ��ʽ`�6��Ǯ�8�C��Mw���7����Ԡ���Z�\���B)/��F��tNs�S�X���s/D�Q�SW
mO�8*Z^�>:�\�YT�� ���h[A�����8�0S�u@F��3����t#�r�	��؎�a)2im��s���rn'd����u�DaARJ���ɿVtv={J�I��/g&]
;
��J���C�jJ���m���n�`b�4�+�1x��O69!�"��V.5����>�᪨C��h"�r�C[/4��[��yK/��	�Ў��(�ʱ��7��֘��;�g���Kt/���>r@�P��2U����?��AsU�
+1�r3�e�L�U�YM�PK+���9
VҾl?���n���iT9�f��zl�FD[����Z�ꬫ0,^{*=�0�v���d���K,t&�'�B����)iÒԦW�WFt�ƃdt@�FЖ��GB`�r��K�eM�q�,��P���_� @~�z��gk��q�%��R/��y E��'��ؠ�P���r�(E}���5�^B�z��9G���u�Q��s�6����h:Wh��[Voѫgή�����)s����^VC�����d�D�i�L]X��x)�ӑY���_ۅ��ȥl���co�Nq~��۴�U/�0z\���JZؑ�ы&�\ɍ�ؠ�Em�Yj1�Y$�b�3Ya�}�+���î��[�7�'����O=����O0�!c�%��ê��(�k!���F�
3żϬ�줒�
K��x�^��6�F���:��Q��<m����H�+�h1��V�_��>a�,u$2����r
|��0��p`x,v#����j˿�tTjQ
+�Y�= ��ڸv
+�F�?�@�`���\�!�+���/���5�QR3�}��O��[��2����cm�Z:h�r��|G��q�4����Z�-�Wi��X�#���M6:��ak���qY|'E�4%��fG�+
~�jF�9Jq�G��}�;��37X�x���N҄��T�#�\�z�z0�b7F�@K��W֡i���siH����"�� {����2�Dd�_C��q$f`��5���MDz�dGO�9E��R�٣5���^Otn���sgK���;zcK"�B&&a�u�-��[(�O�%������s���y�c�<�ۄ�
�a򸋙#¶����m�'h_h.9]��a+&a� c�"�'��C���6��
>iOI;f��F�1�s�EV��)�PT�Dƺ
+�#/Z'�:5�:�ʍ���!����!�<-ȇ��5��2O����n�i�WJ�6"%�Ժp��@c_���pm�,�w�T��A��.�7�j��.I)�58�db�cy�Ql����\�V�����-N�Ǵ���OE���m�.�g��!ʅ�)ߥs��d�+�f�t�*dF�h4lY�XF�L�	Yp" 
b&4m��Ƌ��j��%��*%�s�E�������a��+�Z�h���N����>�b��x�0#��$�X9,m�7stNŘlq�M��O
+�_ψ������1��wQ2s�,g�m��$�*�Ʌ�k���B��B5��$�R�
+��Dd��`X��ģ
+�Gj����%��,��va��SG�������
+�L��(C(7�]��4���mn��덷,�t"2��PWN%"5��
+Z�A�~�Z)
��w�Ђ˪�y�)0j,�F���.�ruj�UZ3��ƭ"}Ƹ�B�7d�Y�x�M���i��+����ϖٰ���zP����(Zv����8�C��Fx���1�Q�.MT�f�[2��&sI��g�E��b�.bz�jݬLҔ#�<�n�=�N��DD�A����q=E-��TH��gn�M�Z�E�M���~��ܔ���iS1D�u��{�$*l	��*c�҆I:��A�G�	/�f��	z��ҫ�\'�6(�F������l�F?j�9�\�C�&&�JEysĝ&g h���/������.�(@&�����!{�U�3/[���X1��w��4��C�&�b�'PR����+sh�,0�
�4��h�X��>z�E��ǹ��u��06Y�7��*Z�����i��kdx���@;���r}���s��6t.��I�׺�����^ݴ���X�r3��F�#
+�ѐk����}���D�����u�`��ӊA�A��Jn,�߲@����
+��|]��Z��6]P�%5cv���)�T�����0�팽i7N&9���cu���;�e����L.����9<��uO�	~w�10#������d����p�(]���3��9�L���^��6Ҏ�a��Aځ�j޽�ČTwG�@,���!��
+��h�j�����?��<�e�����n$P�ӱ���u��+�-ڊ1��Q�}CT<�%8�"�d
+�
+�`.?���t*N��8��K�`��u��-�8b��FcjRhyhb,�����˓:�\��n�n�8�~@έe�Q�5�RoC‘��<�6������y%*P��*�Sbc�����jN*��
+�r W"�5��e5E��lX��/�nc}�y��+��A,Ѳ$�{�D���:�C(j3���%�O7w�\�ӎ�|(�6Tu�r
+�"�HR��S��{bp����$�J���ҡ�Io
+i��Q����#7����E3+9���h;�B�͐��b
��6(~�?Y���G`��aǸ�	�Bu�}��s�+@�U�-�mˆ��<�eDr�2��T(� 'r�^;�.�9;�6D�T��Nf�)�
+BxZe���Gxg&jW��3|Z&#�W�5A܄pZ��p�K1�zɡx3�gXh����f�\_L�e"�<�ڬ��p6�#�H���n~���b�"7������1 l����� _��_���ޘ���
��.Lm�&��*ט�����gw��k��p�S�1#�ö�R�u���t̴D\�d �V.�)Ϟ2�!�o
+�9��:����Ȯ�+!>;}�2ĩ���$�"`�,�Jڞa�,��7�=����E�ˆ2��lQ�U��x����
�t�>�Z��;��&�x(�����m�-���͐�t���~���(��T6�M����,�Y���* $>��Q<6�E������&�v��f�of^�K�������r|˸����OD2G�_�0ؑU �~>���Q<�m=�{��u,���8t7����()P���4l���<�\�I䂌�`����cY%,��s�pY��U'pW�?K���=c׎&���F��Ĭ���i�աk�H|���с�ჶt���B=�H��
�K^�
��,���7�QF����u"A��
bU'JM��e�F	9]����^�	�̅�^�_�ܴ�,��F��:��������e<C���0-P)���O�V�I��=kVR�X-¡�A',���=d��6��S~-|\)�����}%�%����q���\C4�i.�@��N�ڥJ�1d^��g%��O�|�+�)�Z��!���D ��x�^�g�-���7�M�9���L%��~��m�,;Av��h���!�C\�����=��_8J`�#�b��z5�R�g�MA�C��0^�dd��'���3t��1�׀*��m9�q�,��n�Xg�{�&44�|w��1�Md��-�i���ң�=�V��^y��
y��� � @2��6����|hQ¦9�N��&K�}�N��*����5�['�0�2.�r���!��1*;T��З�p3��x+���
+�b�F�3��!�R����8�#"��0�R���_8i�r���!�h�����U&�r�ޏ#���|+�ԗC-a��EҒ�p�㡘�=����	�	ֺ�[3����:we!ռ�����<;�� �'q�Z��'��%]�YT�[a$���eD}}!s
+#���'M҆�-r{��ɨ/�hW�]	��4�Dɜ�9�cO:��u��2����(�P��ۨA�V�d��%&^�No��0-d��Ţ�����>_Y�г~³X3�PB��v�	�r��{���3�n�(��8��U�^���J�d��Y<T1��:�D�c�?����n�J:_�ȑ�?��,�=G�~���X�)m�ga��z׹-��q$�L�%�M��Q��FS3��M�)����a�������e:�2�)�5T��S`y׃���JÝ`�c��q�^0
+�G���˱i�"�"4�V-j�!�׊�����14�j���kPF�j���AJfN��!����K<5@��8��T`���B
+:��D����pa倥����s+ǁn
+�YQ+��Y�R�j�i��CH�̥�+v��D��uM�7`��6G�����c��OI�rB��l��a!(���VI�g�2�/MJ�9��[^����|��""�-�HgA�,A��L]�K1��i
+�%HgS���w['�*2 �ӲCTh��WK�+_�;3�6z����;�N(����LF����'.,����a)���f�?Jek�����c�VV�;'�8�"
+��u&��� ���HO��eL
+�ۍ���W|\��U5��ahc^�Gc����5�%N�۟�SԠh��alu��
+�� ��:P��D�D������.�x�[|t`GWH�0��mK�)���Z6eѠ���\֣J~��u��nv�ɔ2���M帶Vᲊ��v�nq��e�2Y|���$�0{	��x>��ӌ#����^���8h���<��|]@&��
+{E\�9�nsH����ܷ�8���yuA;W�z 6B0N,�(����9�X���2m4��ut�*����p��T�)�'�?s	���� l�gPH�>[�蝫i�a�p ɿ,b�#n��5�hUzF��g���$�)v��`�zq�ڤW�:&R�&k#���:*�,����	�P\�g'd8
�
1B�]_��ymh���xq"�8:yI�+�|G���\>�9@(��)�aҟ/��8~�G�҈�T]�	)���w�e���[T%OP�8
+���ږ���P��q�M��&/��&��kʔ�c�#S9���2��u��Wc�L�$.c
+���wui5����ea9���|�%�Nh��*u����6�MNt7mI�q��??�g�`���@��̹G�A07i�#����J���6"����4��f����!,��-��f��%�E��K�����Xoti�*����d�2���
A}&@2s��z�Era��6#�ˑ	*2�[C�*�<',Ǘ�O�:! 5��g"!ҕ�7�!;?A�>��Cc�ұ!�d�ɺ
+��wTh =����\v+5E�!
�|ql�c^��$
+獜��Д��(9yU�!uXʉ��V}]j�oQSL�	�btS�Pq�9����@��s>�0
+�=Ҭw�-��v5���]11��b�_h����-�,���Tjt���]@M�v<�"�[;����C�����[�l�23Zs������My)�$�V��gq���]`��H2CYz1��0B�'蝁�-��jq�"�Nߎ?��o���w)L�%f�����}��[RG������BA,��K{��2#�z��%��.�ckc��rOE��*E]oZ_DL�ꙺVf��m}������,u�iz�s;�_���R­ !-Y�ow=�N�5�����1�L�^��*�K�/BYC?@�'��H\�6��7�܆^��B��,Eo��W�6����%�ȥ�����ߥ��D\��Ҽj�pV����z�V�����;�k�f�Y0�?�\K��[ln4@G�>p�XͮM�ǐb��ͧ����Bi�‘Eu}`����E6���N�|��^V,��?��	Չ��7���O�L��nC��U��&%�y5�?�љ?T>4Pz�G�{�t�`���ODLp
0;0+��l�~4�qA(�ۘ,�Gy��y
+&N��p\k�]6)\)�Z���_w`�P���K6̎Ĥa�;i|٫�Cվe��mX�*!+:�Cυ^���/����v4<�jXa���E�@��@M}|=֤w`*�!*�R;y/���S�7nH�n��m9Eh��j�)
��5U��f�E-h�8�O��	L�'E^�l�у,��BȔ���tA� A�z��]��j�i=N�ֻ	X�y+��u��E]���T�;�ng���g;�r���Ø�ӳ$���U��/�wJ�c4m8�5�p!��Rp5.͚$Y�vMg�L}Y��q"�9�B����g5o����U�MHg��$|��V?���9�-���ܳ�f5qjӆ�X���P]��B���h��Q^��G
+@s�F�9Q��T��*��L<����u��dI_�5Q��Ǧ���+�x9s�K�;HU�JV
+K�]����Y�|���58"
F(���0�X%��PqP?y��$����8���(������W��JK*���X �'è͗PW3U�!�@x�di�9,p��Ɋf5�JҲ��J�2Lӭ�Yg�Xe*�"L@��%�`9�9R+2��*~
+�0�vYXlD����V}-�%Y$�,�����K���R�Y����1y�A0ERq��:��`B�op
+����#�P�&P��]�<1��b�uB1&�a���c�c���@d��pI&���I��T�>귁`"4���r�qN��YS*�h$n&d�x����8
+f
�ѕ�<T�|E�P��6����&rn��W#��X�56,/�o�伦�"�t��G�F��
D��QV��^|ȱ�a��鉙�u���3[F#ͳV���‡,�'��ϓ�09a,`����W���Dp^�Ix/��͑��p[�qޗ�;'���D�P:r��z�4h�+'�%{�C�e��)����}r���ə�k���'U1d�^s$�"�X:GT�ӨAcC�&ȣS)���P'��� Nu*�:�@��a�mD�.v*:;����kǁK#��Χeof. ݹv�Wn�N6��V���t�A�=���D��T~���4���8�U��؆
+<'�!�C������ŷ���m>����-߭�Ԟ��eOt�i��[ ;9�R�wt8�}����mt*���Rq�?ĻZ�I/	EN��J�j�L|��|*��>$�d����lk��9��ʐ�;�o; j��zw�1�AZ|��;�
�,}��D�KMs�R+�I�0�P�Y�;@W;�rKY� V��-�W~	����}f7w�baT�:7�ԩ�6zŰ,��QE�gw���l�e��?��$(+�@뺯�ь"O���@���.���ML<@cVm8;�ΦFI���|��}(���&
+�z�\6>)��xn�EZ��;Є=��a�e���'��Wl�2�,���9��w�+{�ފ[�Y-��@�y��\�7o�JFL��K�\�{��h+�R�	Ly3�A����pw�ܭs�r���@hٳp"�x͖s�^��q"KM@��>*<���T�������/����y�c��B���7} ���|q��b��)���}�F=bYMh`yͩ���P�]M�56��,���8햮.���Pg��-my�1�!0����,9��xX�SVm���NPĩs�qC�A�����tYZ�oבn,5�#��U9����)%4~#��*������&�!��_l��Tu�
+��-}H�4��@�E��U�;�DH����,�Z��X���اq<�"�U��pQ��;����`k4-���0�t�T2q�h�����\�Ľ݀<g�����h��7_"љl�gL���;)���������al�lS0�-�B�q�z/��'Cu�w1&Gڞ��4XT�ש�ӝf�_�UAʔ�Kxu��G�&^?���9�e�w�w	�=`«ՙ\�/�U��c�
+>—�P��*[�-�|ߢ��Y��s�T�+�
+Hq%f�(��#:��A��[@.�'P�h/H~Q-���,�t޷V(3I)���cf�#���
8߯J����<8�+�e���K��#NY{�{�|,U( ����#urDn���N���dz���h\!5��5?��ȗ�n"��^o� P;���42�XeU(��.��Rak("F��QG�-��9���"���O;Bvi��'�%oG�����4~�|"֓Jd�J�M
+��T�Y��Xڌu`2%o�K�g�^	�5��u��!���dq�E�X~��1�%�@)7
�H,�a�c��0�G=ɂ
*����,UNAΗ�v	:	��`j(+���~~��(EY��9��XO:��P������E���)9vs G))�k1%�l�������Or�5����w&m�v^�d�`]%��w�g��G!izp�H��p`��t=|�쐬\}8QHN���d�J�}$���_�=?�G4���#����wDw�a�Cf=�Y�( FP�!r!A�,�<#
+���5��Z���-��Es��Eu�E;��{A:t�":�M�����tv"��&��Ͳ#'IJ�����2�:��6�_DW��c>O��� 
+k����� �=�	z+D�P`�ȡ+ҷs��fG��J4�]�۔u>i��O��#fzO��<��ueQ�B�+2��2[t�Bt����e���f�,v��w�j�
+��ч>z�A�3!(6/P�q��x��S�3/����B�'�|�Z=<BT�B�xH5.��X�!��c��p}]H���Zv�?�hm�P��J%�����Td���ĥ\H��++lb	,T�r��P%�� �=!F�����=�k�)���-g��a�^�	�](���DĴ˖��	ѓ%!�󞐍<M�h��v��/Lg�	���{`�2�)��(@h��6dn�eBQ��Sh2�u��B���C�I������h�$~!��,�d�=�c��	��J���l	i�5�j�r�/.^�h�v��fEkDP�)
+�4�9��yl�x�FQ����SV�c�	�N�"�Y���%��z�X���^�;�����h�����][�9Џ1�:��W?9�u��
+�����Z�4$��`�\�J��dv�;Wmȗu�;P}rr��H!*ȻX�g��x�qt9j���p����e�!�8ʗ���k͏�pι��=~iN�<rc3�o�]� ���$�V0�7�>������x#��D��W_�AY�CdfSKr�q��r���Eô��6Hd���[/.{����Ɲ^l��ֲ�p��k�4%�ˤDeaC�8a�������'R.����'-0�&ekZ�cd$f�!u��[Tc�k�;je^k}��\��j�D.G���ׅDE����⽽�}�
��(�Yl�	�L{.�;N{t�e��{�*���
+;� (~�
+�}x\�,L�%YLȭ��\�����?F�>��2���?!���N5�E~��L9�~�jP�|�p7FJ����+/�C�s����J4N�S�w��(�0���)�*���Y�'�e���w�k�>ewk7���ҧ���#�j1��@G'��&P��<�(��%U�;桓����/��~�v�Y,�j�щ���E8|b�)�y`��l�� �%����u\��.����������?�η��#��H�l�k2��f����V�%Rb������l�rv�S�/ժ��G͋����q�2�������߽�R^�{ �[	���F�4���,Z��z��WN��3�qH߆`�@����V�d
+��3�_�}�[
�bcɩ��S�W�񅕝8Z�r�`�,�~���$�**�B��
+�}be��ad3����ۄ��R�~�РfM�_w琞��C)]����Mv�Ճ?n^UHH������=�,�7K���ְ<l��7�<>�����`�����HQ�X8W�[��]�I�;Ah�w�f��F�Zt�;P��Z��[�FEb���ţԳ,'PV�L��Z�9��!v���3�U:n��Cga�!-�$�z��@�vZ"��oE&�	�@´l�O�|l�1��M��r��C
e;H�-�c}�4H�ϸ!�~�2H�}2
+�C�΄�!
&v�R,�R*g�u><�+�;����� 6-+YV
+�!�Oş$�/3�Y�C��C�N8����\-ke(Z��:� �.�jcE�.�9�Mޤ�ZF�1�QTh!BQ�@O#���V�e~ƫ��`��.��`�Hu�@��Sb����X,w-����`�mЩH�{��@���Ъͻŧ=[''Q2����;�st��}��|�����T��J�Y�}�%0��[r���'�D�n_�B$ؑ$����P���ĤEeWl�0k�M�D���[ö,d�g��`��I49V���}d�U�܄%'L�PcXV�ccA�s�^#�#L�ii�(E6b윉B�����WQ�A����\���'^�9�8	sN�b�L����p�'��uI�ixW�Q��B�QoǛ�e'a���H�q��z��i'R[ƀ�OAGxAqq� ���?nܡ_�?��.R���2�FCd�ܭ��q�P�34
+�NQ�{q[��%K���q9��>��Z����:�6*�Y�h���j�?^��*�Gy����3�:�8k��X�F&,�cf�Jm�hQ/q�&��!���`_�#AЛ�ڲv�H|�85��5�L��6_CNu��\�}XhD-cÈ�ſ�o�"5�5T%*���r^5�,�8֦�@��>�<�_������+�$m�<x�;�KY�OΫ�۴��@dY�<���v��
�
+��������G�|+U
���L63
+�dt7س��6�*&r
+~�u���� ���g��$�t�.�ҁU�u�ȥŰ�����1�#�墂	�\@b]L���$Ҩ��64-T/9CkU���`]
A@��Hٛ����f<���c�P�l�پZ�L=Jk^�-�_	W^0�
+.�=\8p9��� Z��8�kȜvT�l��K�ӭP����Qay���n��/u��H&i�k�%u���M�6a\������
+����ѥ��PC�?��/���jj�$���A�<�C�\�<�FQ^�kH7��+�"^�dk�2���5J����W,<�δ�N��!ȳ�k�q��+��(��|Q[�-W�G_:�T?R�ʣFP��W	���Fnqƛ�N7+�wY�Q�i�"�?0�%�gm#�g+5�UՍ?�.`=bF���R'����zF����F����[�)��/�VIuY�o�TOXE���n��Uaz^�Ҧ��~����e�*�������qE�F��A�J��ΐ����x��Wp�4[�ϔ�	P��=��%�aU�J�x�`R�Fm�:A$̅��(�k��铂�X/u���m��*���(��^��B¹�����'�u�T�����#�,�w���~����NajNwj�ucp�*�&��&2P����~١��<T*����-�Api6z�0��z��ڇ8�Y��;�.�Y��?8���^m�&ܔ���Kh��=G�Q�j�~퓰+N2"�S��́J���[���oqS� +���#�e��,���7Sp�39{�$��	�*�(���&�3=tH�e��-*c�{:���W u���� �E·'�PA_���݌|�}��{
���k��uq-�1f�f�+���j�W��X�:r`Tl���Pc��Z~P$�a5�o�jE�=B`k	iƲ
+��2R2>]ct@�V�h�aS~�Y��.˄k�ڸ?���2��^��Mn����+��f�
+�`�t����X���09g�%^�9x>��TO9=�?��/�2`
+��h�nQK���.;ފ�0T���a�Ѻ�+b�s�{�Y0��4׌#>S�PR c�cb�Y&1
+�R���ҥ�ؿ+��u�q�a"�-5`l�%~Ӹ$��)�֍D�.�W;�3�MW)�K�j��th8���m�}�3^��� ��.Cf������P����\���[�r1v5)Yxc�4Ɍc]�#? �6!��8����\�_���vZ4rY���A���XY����ʐ��囕�42�?��Uή:�\�,�Y���0e8��GJM��_h�0Ā�fEL/aj.x����Ԥ&;Vk�p�5A�2��%�;���hp��q ��+�w�$BUتݪϟ6�X"w�+q���N7%��e��r���›�'�VT�����_��Wc	ljt򰕛ol[�J�LC�ldK�t.���Β�Yh��HK-�M�u��M�Ғq�D�4
��������< >�k���S�k���]b!<w)�����f�HT��{ĒK��ǁ�h(���hM(Z�1�3b��^��ᴟ���;��A��>{���3�`����K�=YҠesu��4��ڗ1��h�
+88s#����
СAq)�����8dpn��v��x�r�F��
?/�Ѣ�b��Y���ġy��%�9�Q����z*���B��v���b��}�!"m$��v>a���A��X	��b�M���Z����N��O��*���8�_�;�M����O�ZO���5��:��{��=IBI�L�bj��u�C�8��8�q�;Nq	E�9X:�N	���|If��)8��0�S� 0�If	��f�H��o0�MF�G�+����"-�;��0".%�0�[r~L(��b�c
+#���a�#�& �j;������n�d|����N�I��Q�@G��VH�텈�H{��N�-b�q��F����1d�4���е+3�<�C�����C@�Ʌ��{���	���Z#�v��?��w>�6����K�X�߼XfF
]V�a~>��g�z@[0���Ԯ�]�Xl���/�������ș�_��z]W��,�:������o:�SA���}1(v	[�z�
+5bJ1ZQ���ƽje�PRodF��sV�[Fz]+����j��:/�t
+?S���:�'�*��4:����DǟD��3z]�!E殉��MR��)�P���x��ل���q鴔�!�����L��R��LW���~���X�y
Koe��Y����7�Z4�}�)�^X��7S&󺊜�D��t'�N��&�?��w���-�h|=��_/�p!�s:�5��ƏK$��8��R�e��}67�h:+�;�Re�4�:Н풯��Ot��r��w��c&~�N�"����g1m;�_�ַT"��ʿW�p�EU�� г�x�#��͍@�٧�Y�l+@##>g��>�cD�#�.�2�P]�p৹�˰mQ�Œ����W��P+c��P���0"7���jع��k�M#-�-<��!8�T.E�E[c�]�'t�nxLS1�?��(�8�Ϳ�U��Y��&�C�:�~�"3aF/�|����#z�{��q����������qo�� OrfG��uŊ��pF��/���Г>*�"7ۦ���ǯ��-/�w�~�d��W�K��[��-CA�I�;��h�f$��h�AQ`�+�^��a�Gg�ο��I��r����PP�`z4^���b�-��Y�z@�X�G�d$�6R�����/-Nk�aD`�98�nFo�f�&�q�5�A��q�!��*C�8s�RCR|*���LC����Y�R��d$����l53�N!1�}�4~2O-*�bGc�90�ϸ��X|+��}�r��NEg�V�oEۂaj�'�.,�$��Yi}�\{����
?��uH�	����m�����j
+`3���h�3j-�H��i_vKq���u����+X�WYO�9����<��!W�[y
+F��F���(��u�D3��Y.�#��ӢP�=8MX>�=N���P'I;�1r����k����!4���1���챗���O~�mH���7"��3�ZYZ�J����jr�Rz/��e��؝�X/�K�k����\xn��{�޿|'s-���y���>Nս�k�P.A���3ǴucbB�'����ch��<dK�CF�/�12�C��^zɰ�q�‘�Y�28V�e�7[8��*�J���*�?�����i�e4�;ϤʇB`�5�^ZQm���I�Q9Ӫ��4�H�*�(��f�.�E��
+u�Z����c��"Wg&����j�閛P$0��?-bhYE�䗾PK��"d�v[�+��(+U�����E 8w���9��ǜ]��}E?�Kѥ��h���fU������`���Vm��4��<� �
+�Hu�;M\�&擴�4h�x��T=$?����T�bBg:	�V�8���i�c^�'�a*���~b�$ẅ́��L,����UGW_6g_������K-��E�V+���h��l�D[0	3�9s/.�/*�b��f�&Ҟ9��(�����:7%�H"����9���V�K��������K_�PMdԓF����E�P���39ߵ�?�)�(��d����8e��K�)5f�l��z��jG|;^�f�����4�T��O�1�:?�����꽐�o�0��w\��	��.����}'^��w�_���"�
+�b(�>񂹵�~[V�-�����jH������Y,���T���o�?M�<��Y���vE���N��$����'n;8ߟA�c�|>�V�}%:�#���M���-�$%?U@����P*0&qB��������r)��LJ]hϱ�!D��"W�\�NW�������|�d|,x0��|
+.���A$�+N��%��m82�.���
+3A��&�b��!�ZĢ_�����15;URE�N�^�*�c�m\c2/	�	�]i�cy�b~���j�G�L��Q�����WS'�.OGy����w(�b��!���`�I��"hW`:���%A��f�>�-N�T(}.����1
t�Yt�E�L}7�٦��d�l�9����~�,��4]��`���
����:�!�����@:��\��F�sp�\{>9$����F��E����8f�K�N*d�K/
��U��5��l�Y�1ՠ_;á�^4�ǫ�hE}��(������x��Ie6�{
�П�0]��݁���W�ڞи�[kW�����eg�����v

�8?��v�<�a�㏞�(�3�a�*c�
+�
+,���M���;�J�`VM/�O	�����Rz|�Id	q�F�7��ҏ��s�F���j�:ҩH(����ԙ���e�Ïx��Qn�ڵTw#���%{^}���h;9�̀��EB	+�+!l&�`���ېxGJ繖�Qv�����+y���\J*0x��a2��,����'�lȱ��¢����S�](��S�r������GL��+�E�'����&�����Ld�� �kC�ꎮ7��k���{J�w������������#DY.di�l�
+��࿐�:jk��ڼ�›�6ӱ/�ȉ��?�JTa��J_�C���QC�۞��7Dw4�X�<�_}�1Ґ���6����G���>M�q�O��=V�N�%��3W��^�"�;,߰	D �u�{�fMޝ���O�쳶(��ji�a2+�{�C��%�z~Do�YƸ$����'j�f�b�`H����X5�˨(eHFߩ!Z7����6��j���lN�Bߴ*����-��_D�Ђ��>\�;>5���p6K�����:�B?��o�jngtX��oe�q|����鈇	���`�	�O�qixYk�!�-�8C�@<KTxvC�s�K�� W���c��[8gPn�O����T�lA�>̜���|�KY �,�%���t�WB)C�/�n��~��C?���ғ9�ao
w>�Ҿ�&�V����]�p�E��6�!ӝw�oݞo]�M����	���X�=LD�9�b���*�0jM�,c�������2��������7�����D�[�����R+��9�N�(T�\�RS�,d3���?�V��)�R3^�~�t�9��!��"��,
+V+E�2��#��rRz����-r�T�R�0`C����ͨu��(��>��6���c
"����vY��T�u���k&���h$��k5_f��i���DijG���M�����
+��֕��b?"t؞z�P�g�M�X�ƗpR��肼��¨MmEƾ�!0p(�,��}�X�62����0�i�c��U�V����z��̢U�u 7��C�?�S��͖%���PK�rb������j��-�\z��?55[����vV
+r��<&�v͚o5A�-�����hB�;[���/�"��rh�u�\9�Y9���M������駧�D�S�u��Fx����U�����GЙ��|t7��jBU��+��RL8����Ui�z�ƌ��*(��!�ԏF<��X����n�:<��E�y'�4��
+p�2��am`�}øޠ�k~;[��DDd����9*Z���;��y��{����	F��붒eͳr���$&�������6�ގ���v"�ø���
+���0^��w�3��}��O�{�̉���*��g�ğ���i��S�h
DaʣP��r0�|K����\P��;�x�uAo��n�a�K�h���?85k�E��̈{6��T��X�߫��b��$��N��O1?�RTvy.�G���^�9�m0ҢU	��4L����7�ث _I���U��.#E�~y��>��@�J�4�:�m]a9�#�����z0�w:
+�8Ӥ{2����
+}S)(a�(�O��$��iϖ�h^fup�f<>��֤G邹4�[��s����@!n_E�=)��T�2���W-;��W�bԪ.֝��J��
=^ʍ�/����+|���l�2�{��X1�i���a��z�ej�u3��?��$^C�W��MY��
^Aq$!�j�\�x��*u�@�O*"n�1�V�a�8�8�ޝ�k�Bl+Y���=�$�,e��"�J���'��o)3#
����h�
T|:����]�C�V��S��%����aU5C#�6S��m����ɱ{v L�s�AK@�Y@��e{���b>�ή]�!S"�|눲�.��l$:DVÏ�<���9�忈�1��$�!VcQ�E��K�,wa�0�.ֲŋ�^L@����e��P�5K���	F�<C.k\E��]���+d���ebp��HɎ�噶��1�d��H"�������������ρ�s���1��Z��>�"��_�~����QȠAĊ�_x�߹�7�S�mU�Ϥj�����1|(��xꉜ���y&�K/�\?�c���D>*���q���̳!�s�t�;o��SHX���@�Ŝ�� "9��<i��r����,q�y��d�2̾�;x3N:��C��&Ғ*�]�G���1U	j�4����À3�����.`>WKe�O��;\��^Zqx�4/��;����w<�p�f�~���A�8I�J�kp��k1�g�m�H��d�����J	�9U�C�4�͓~J:o��{c�1֛����Nw�@�<:�5:XSkV/���'K0�r��1�uY"�&�5��T�:y���Ayw����z�D(�oD�`���y�|�_�h���(ϒ�G�2k�����\~Yf2�_��3��?��3%��@�I/�Ez.~��ק^cb'�
������:�2<?��q��X��e`��
��Rz����DKW���4�?ew�@f�[���J��+��WVV{�$�]���
+,&jN�(p����V�-�)p�p�K.���,����UEm[CPR-���'�Dhq��MP6-���ȷaQ@�k��[�����4��'�����Md�zg�a�;�5��K$3���B�f.��*(Q�ˑ�/�>+,xۀ��ёĪ�&3z����Tw�ԡ*X3���"����7�3��<�vw�+_��Z}4E,A�;�����T8�������g�B{s�=�A�t�1-� �F��$q�|)���u���Q�/�W����0��#��[&��h�]�8A3���#p���RAp�;��u�L���J�m�_J������R����*�08��*s3�E�E�Ta���]I��#9.����d�?+ڷ?�&$xPU���f��q�x�������*HTYu�U�G��5���7���	����k[�X�t�f�� �h�ҳ�:�a[��n�4Tջ��hg�΂@pAm���tv�O+x�mJ��9��+M2��@�z�¸�Gٜ�ã�h�pΥsZA�d���F��ZE	n�d�&ƑG��,���R�as��K�����d'6�o\J��=F|[QͰy��m���a}N�C�!Ck/i�Es:ߠNڑ��H�M��YQ��1@�}�����%*�tK%��oO�L'�aR�K�꣊��iI%���ݨt�@�c+/�����< z�!v���K�-]EU��3��B�}���K��Ep�:�r
�ri{r�E���5n\�����n���Hx>���-�N‹�d�:Rc�3�i�z�^47��X?RB9�����C�"�m�����-ַĽ���^�Ӛa{��G}h����M���i���^�:�>w1c���9$	Mk8�ޜE�K�"��:�1���Zٴ0bA��?e��Wm�`�)>YL[�r�
+:Sa�:0NЖ�aDɳx䚒�8���:��ч��#y��~��5c%� /���+�<f�Ų5
@�˕+cNZ�����+�k���r�^)�E<��9tȕ;^.��/~[qL���ii��&�p%{�HZ��X\����x4q;"�B�n�w����5g9s��s:���J9ѽ7�{��L ����,\2�ߓa=�>9��͛b	������v�i��H: 8;�N��AJlQ�-p��ۭ�¤�^ț�ƬY
+�R!-��gd���&�'VWꗕ���<�/��Š����RE6S�E�5���D���3D>�
�Z�I��/��K�BrB���=ఔ�i��QE��n$���@��THP��
+I1����$z��Q�җ'���OYP+��Ј���}V/�2�RcɧH������4��ʬ7L�:�o�ו�kHODo+�����K�^��9��]���R6g�d�bYb�K�C<yM"hA�{6���Ώ���ў��OpB�t�HIQ����j@�	��1�����s:JY��}b
s1�'/����R1Y3�*��<8[\_�"���#bX6���/�a���m�B�1������Ѻ�ʆ;�"Nij�q��6r�%4��QC���+Nʅ���m>EC�����jWc<6���H��˿o4����}L?̉�A��T3�nzeu�
+g��za���2@���r6�3�wN�p_�\Q2`U�m8��銨��t�Ӊ��@�+����/�s�r���=;9���;�9���P�s�j�D�hI��N�̪$r�O����_�%��{M�&��T�-�n�|��?{ٻ��܍ƺ��[`4ҵ�	�_��OЪ�S�[��˴�]eh^l�+e}Be�q���aw!~ҩ2�V��I��r��vm�C�nV	3
+Rh�tL޴���b���`�㢅��L��˿oY�Z4�������Q���Z�)����D�!.�-<�α�T@u�p+C�P|�9��3b���Q��_X��{Mrs$q�$�`d�T�,�7¯cZԢ]��'�SM�N2P�.Է��b����YB?�rǺ����Y�$D�
��ƒ2|�1T ίgl��s�`�)��M9p84�'g[n�@��]J$	�&^���)�=�q�����rH�r�js��dd@�����x�E��`�>/$�������nZwS�}��4̊��Fه/�Ѕg����,0l���a�j6��][�tEl�ppͿ\#�+3�Q�R2[�y╚�1��U����2����b�k�%��(�Xpp��5�`��Z�|�
+�x�-��Pv#qX?�|�����0���RfֽR��N)i�G/�-�
+~�L�:HhM��zy�w���6�t3�B#JHK�Y	k7�S�.d1���
L�-����X'�ev���}�ٽ0���&CQ�`�Z�b�xUE�W�JJ��,���H/�o@$6)�be���~�?��d�š�mn���H.\�$48~*��[�ճ�[t���*J��
+�H��"��!��w�Wo�SӶ񞍏1-�P�ϊs@.�M+�@&�>�?�ױ"H�J���m��j᳨�C��B	/��i��4�\P�h���\�3T��Ha].�5ڒ-���Q`��|A
y����M��#��C�拲�1Y�����J��g�\�MN�j6݅�)�0 �Q�Qa���5��
�A�h�
+NxV&�۬'��b�L��@I.9�5d��~��{xRG��bc2t�ɭU��f��U�f�JF'n����	�����Y��iه#S�%TK���xu�
+0X��`���v�`i=q-��"6ϵ,��QN9*�F��j�s�r�)��YՔ ��7�$�$����-c.�B�\�Q�4؞�t�tQE��=٘�5�7u[��N�j��q�I�g��U�Xȹ���n8nF&�V��E�
+t���5�Ƿ�'��Y�q%���-�6;�ި>�p
�Z��,|.��ɧ}joaΛ��4��d�:{=�����_�MO㡏�4.�҇�Uö>�5!�5ɳ�c��}$a�w�����~�w�\m&�ϻe7�޿u4�ϖ����������Ǘ�৾OF}�:��ޙ�!��Ù%-t��m�>�׉ZK�����L���o^�1�,��l
+iF�^��T�D�K������7�p����Ƅ(�8�)�w��%���7pq�H:��	����w"��N�'!)6�ɡ1Yfv9��Zda�~� ��8�Jj��;چ֧t
+�7�L���2�������ڣ�] ɩ�4u�|J���5�bu�w�y�1�l������,nϟ��X\��,ɾQ� �DP�.ak�BPB���V�kΚ�Sd�����gz"#2�)�ߓ+�� ��?1U�y�	�:�a��cd?�qGi�fޕ�~�ے�y��a(��&
\�~LK��I*C�f���O2��4%�T�ݨ�F�C��w2�"_/"�cx�<+���K�Y��3F@�T{������]9t��y�&3,^��eH~#���W
8��R����ֻ�i�(<���
+��RP
��B��}@3<_y��2�$S	�g�Y���$�o��L	��h��F���'ڨ?��SzɯV�تr��*X��
+��������Q�f[�X�16-E?ߡ�B(�Tk���:�M��^���bb'O�c�xd�q���1�������k;����C�G�	��tZ)I/����?�W�+�	��?�h<�	��x>��UR9%ahl`��j�o]�J�Y�A�05�rx
�U!$#��b��������ūpQڣ�R�71;����UP��T�e��6?1���΢�V����b7��ݢT���'q`	�js,�C2�
�1�*��1�Ig�*	BQ.)S0�L�T�NT��3pb����
����������L<m���O�fU6�I���D�QB�ݝ�N�

���lZ�Y���?Bz
+���2B+z�G!�EA#��MP
��ի$F�WS5�1S��&�)q�92�	�R���Wy���?e�̱�X/^��/��rwSԑ�h�z*g�xBɐL��#]��XH�89|x���1���-��
+ٜN>�%�<d^�Q,Slld'�.\]��c��34�� ���yS܊���Wn|SۈQ�m:5�B�:Qtxa��%n�L�b�Gbf�	v*� 5A�� �>H=HI��^���yF��J�J� $��
+U�����T���Ղ���\o���b�UF���ňW��tDE�V�QUS1ՓݐY�"5B}��g�G�yV$U�N���yq"r�21���(~Qƚ�/��F���qE�ɦ�U ��x{�x:eX�""$D�B�@�ɰ!J�b���)�ӁR‰����h�!	I��Ph&'Ā���V��$��A"�KF�A�8C4��,%�c���~��
+%���FNH�
+"P���C��CDC�$D;�,T�G���.�t��D�K��{�P(�vQ(J��v	$��VfU���z��u]�d?�g��=��l�K�m�^��:j�MS�MWu�Yp�4�4R�)1v;�-�pK&�S8/]�Hh��>DC�e�t�����u�#�Z�E	d�<��@~�B��t�)	;L��Y&LM�Sa��@�	�p��f�(Hnܝ��FMP���f1KH[u(:cB�*dw�D��!kDL1��i7�MU��B�ę�ߚ�jQXyVaU
���΄U_�Dӡ�P3E
5�F��"B�L-d�y�m1�xA� 6�¸2�xj8	R)�A:SQ�m�f5ʚ�=��8c(�5F���a.	'�s6���)����=��NM�;�y�^�ᦈ��"J�á��MԌ�ޘo��>�&�9�����f���g��C���eǦ?�E�����H���1B%C?n��@�|��4�mE
�/�ƐT )�iB$��$�&?#E
+&7��%2FC�"�j9%!V:J�D
+��tO)D��f���d�%�
+Ev�6_��2��N*�)Y������H!C^��&il�4��<�@B��
+Iɞ��;l��HIH���^���QNf�yVQg~e,94��,O:�R��F�)gI�g6Z��E�_�P��� ѷU�&�q�P�_u��	%�و��zG��Q%s�3�25(��r
+��Q.9t�M�iy(�W�H�����������D����o�~��k�g�[-\���r�]4���dF�0{J}Rv�Lf�ɣ*��M��Yȹ�|�+��FA�y�� ��5Q
+�9}�"K“�_1������
+"�EƊLJ���d;�sI^A(;_3QMQ�_a2������@6��.!��OQ�{=䱽jpRe�9�j���2s;F��EpE{Y�ipMƐٹ�d��.ܰ���B�:�!hr3�t���V���[f�(�6B�@�;�
5��L�Dj�ITI�dbG��N����M3�zƉվ�z�T�帥��A���ϴ�hƒ4[qz���1hzѫ)ih��i�̸����Y�#I?Rޖ�	A�CN2�\7�hN#]�jj�Ch�X�*�B��طh|k�6����y}�B)QGK�G7��4��4��)�L���=ш�
+-�G�NT� e���,��K������i�i�C����h��<D�
P�%%E.E'��
+S��))""R}���$���#�3�>C��e Z�*���VE#N�]�˴�Z��.�Q	�2�[FX�3� ���H��I��!h�p��1g���@(E�U��fO��$xb���8^��Kh�����8_8�p)
)���0�ڔH$���2.\�b��A,��~���O��^�P���}C�u����p�θ��3�>ܴ�o��DC��T&T[��fM(�h-���� "BQ	"!�`�	j=����p�C9��p�*��I1���᜛fg���r�>�6-�/�(�w�fB��oR�ڪ�(*�=EAq(*j1[�(6Ө�bs�c2��hyv��"F,��2���g
��E=�X�g��z�gaU���	�@�.�#R���c��V���*��_����u<�gƷ�j>q�l&(T-jt毵!��j$~�7�K򊮐|u~	M��o��q���%#�EJ��^3�>N�DAzi�͙���B�4j'��M�T-ډ��ň5��R-������(����
+U���蝊�!b2#�"��X|�2�b�D)(:��x����U��b�3V}�Q��
+��E�"���Ev{�%i��SZC�9C��B1A����%Q���i����
+��`��x�eqG�bA�5N���5j�Y��ɰZ��V�	M��i��H�
+N���.\��bU�%d1�_�k��ֱ���/�n\MEKƏ��JPpfn�g�'�is*��U�&�\C�!�52�Ye�xb=�t^ķ4��Ş%n�C񭢠�Eʠ�&Y1���~��!��fH^�X�Q��`g8��h.<�|柺�����z~:�ٛY�}� ��}�<㪬>N��1+���,�ԋ{<dz�jI�y��<�hE�N:�/�l|a��ȟ`ܻ��Ss��fH�$s�
+�lȌn�HŘ��$�L�8��.���q&1�TU�F��U`��4w�xtL��^�T����񊣘�F:K&��ô;&	eI��Q��,(\X$G̒yiIo$�˔�������Hnۺ�\��Pc[Ϡ2�&���k���q��'��TE.S�T	�C6~ՔO@�w�+6v�+\��
+�Jް�J������y�F����Z����<H��Q�Y���<��l��~m���^��03o��B1����{���5�y,�%��$vɌL�H�*WK^$�=\�L�B#���Y���dX3��&sK���\w/jM��>��")��r�h�|��ߺ���͂d�ʘ>!oC۸>bO�'*����Ց���kQ�עhF	��6Z�`�&F��&ĦK�IZ����)��-�M��}9��}VѐQ�A�����ɋ|�֞+��/QP*�Jg��DK;)�ܱ_�m��}?���2zL]٤j&8�Ib"S��~T�)cؒ	�-��A"V猒�H.�#&!j�ȱ��,Jʋ�����ATS�b�MwJ��Q�^F���W�X���_sK�ϓ�ϧ#��!!��R��˴�f�X�<��`7a'8��
+�D�ᄘ���RSy,�1C(eG�M<�&�Q[��Y1��kR!ʪ+��W�P��j��l�&ƋO�S��ʳ��\I��hE�r���QVդH!*��+���뮢+�j���=��a�\��
+�m]ii���qV�f�𙩭�tz�H�Ғba�*6���y��w�h}9��bc�)Q�I8ƅ�̿�ؑ�*x�Q��CL������*�Pm!��� ���u;mM�A�����=�P��0\\�{A����H���8��6".��Es�Z5jM�����F��U�10h��� �����yX��dd(�r(���҉�7��^(�/�P9�����~�H4EL
h�8�h��g
+��0�6�;���&W�v�J��Ś��	��	~=&��Tql��A��%R��*J&�.ټ�h�,�B�D�""�o�8L�I�N�!�PlL5�еI�W��Q~8ϓ>K��n��%^W��d��χ$Xg��PU��dᵛhcEQ2F�YØ��١��ݼ��s�,�!f����PQ%�R�眡��X�Lp�C��o*i7�|����'xC�n�M@K<�Q��^\����<�4�x1�;޺C���W96;�$PQ���TR-Ooq�a��������\�(���Q�,�"��5	R�b�2�V�	QR������5�hŗ�u�Gp����D!
+n���"m����&1Y�a��-�#�A�f-EJ�	t8I"R?F�a$������D*�A���"`ÿ"ь:�>kJbf�4�@DAC�@�X��W�@��P����4�P ?��&|� TR
+|B	�R����+��Rh�a�؜���+���M���b�p�'/���(��/����ҜN(�DO`v!�(���ZZ?�8����S�c�|����)��� ���§�qW	5�W�~1:
+a	�c��a��{a��@�0f��#���	�$z	i��L�C��f
+i�Ղ�p�b"*&!Z�_a�@��,"~�t �����
+S24��Xa�^������B�p�a�@m�a9�/^�� J-%����m(L(
	v �@F��� Rv2T9M�/�H�2���pi��Raėx'\dBB�a
+�S��Dہ�^�N y ��W(��P�T2%젋�����֌ 8�8�K�K 9�d=��$�Zi��+�·��YUӲ%
��#��:U�!�!E�.�@���f�"�A-OB�v��ZʡO��P�D�"�f�Qܬ�,ЃU�2��6�4��Ez����D�c�Z� ���q�(�y���[��݁F��,Lf(��X�$�����h�"0<��4(fG��T������*# QyD�	#3����$<�$�8�"��7E�<2C��FD,#f(�3�DTj�)�)jB
�p(��s�!^Xft&�D!�!+ U�KN���D�0d���|��I��QpU��e8��>��=��4��R1\�Q,4Z�`�I8uE�;\�N�ϴp�N��t�j3�F�/�j���
+E����zY	T�E��Z��0D�Be���(�C�D2��wM�\<�COu^3F�.Q��Ƭ��Bu�k6N��u����
+L��>=�(9��0	8�@�[֥%b�4�z*y;1 Ę U��Gp��b�e������	` �<�P�:�Pad6a���������T�������`@
+�Cb��t���r4�J�!qPXX������@��vBю�GGY�t,��f!G��V�/k�Q|p�οsL��!��q<�u��x]��(+�&���t�:�h��dQ	�uKͮ�;��̤�;.H�1��4�c+�!]U�M*	���aP�~�F���Z�4���G����:�B��h�$W�;*ד�b�\g�H�vļ|F��
+]�����P�Y삥$�Dn������
�.;���"������x���Kr0�4�������I����
unެQinH�{t21T�N%���.��>�PeP��`�4�KC$M��h�S��k9�逆F�©N}t��2{
ɰ˷q�<P�����}鲃�mhv~��D[	��϶�I�8v�����.�3XJ*J���FփqdgL���)�#?&�/p��7��xm���u@$�m%.α9M
�rbBC�Lv;	9ÊN	�O�j�rN����)bZ�=h���^U��u�������������i^TVN�����uؔ�eS�RrP�:��&Z����^Sx��@��DA`���C���!Ie|������Fz�Gdb��`̩���b:�QPcs�p�օ��w�|5c�I%
+\C
��uR�Hc�&�`�Oy��T����c����A��̪g��EG/b�q��v�G�NLim�n#�`+W���C��^I�o-�J��D�y�xi�xgojic=�TN�eS���E�cOU�CE�79��E_�m�E������-���Yw��{~�MWJ�튇�VSY��]ϕ܊/��@`��q�9�"�<}�b���-��f�0�/y�ĜN���̤p���'�{j(�~��a�is�I�Є����5D*|,)�T�
+b�{�~�	��.��t������)����眢̇�����d9�}�Dȯx��6�Tp�`�MGP<�h|��93w�ra�3�i �O��j�����k�6���댄S=�<�ym�x��d<4q�Uu��s�څ�폝���z����9D����fFã�`���%�jѽoK����d�|����L��C�Xj�p>b��`�yNdž�C��O4|A�4��c�,�*��7���ɛ��|!^�\��o�ΎĠ�[��(��OC8|��{��s��Pݽf�`)ʲ|��\���o���Lho��@��i���}�@)���8�ѵ������Fn"{zQѤ�_���UA�Jo�������~��S���#�0�x�=�K$�I�E�{#�D3���εi�����64��oav/�X%�۶ �)St �����%l���H[���G�$���<G5����woQP�b-Wh�0K�^j�����t�Ϧ� d��Kp���~?Q\z#�̜D��uo#�Ѱ�@pM�yA.���
+��kr���̬��g��9��#�h>[�$_�Ү���<"�G��<�w�wПa�r�X=u�*Z�N+J9���7_H=n-�)���j�<���A19��9G����
+����ţf���-�
���ə�Y�Dm��l�sS+k:yҏ��ؚ6N��h�!�ҭ�I�u�*áXs'��o�YrH��y���׈������'��T��R���à���E��`HWJ��j�P�p�[�TNm��գ(�s7
�8D�-���Vt����MQ�0��V/[	�9���6Ю`�Y�y�qξ(�:��D�2����yc�t��~���ʾ��`�F=���,1l�Y.4o!�D|.	��8��/��6g�y+#]�%,�2x���$@ľ7f�E_��Y� ��G�����b9��I��r=�ؑ/�;PE�����=}�h0ӡ'w�L��}R-��?����<��{�+�w�4.���IK�Oe�a&�tE�Kƿ�i����J�%L��(�H��z���U[�H������9.�5����ܥ$0Ry�h6�(f�H���>ӻ�e���zKɋ���߄�
++�q�L��Q�^�	��
+b ��Bc�T�~��h�ݧڪc������N񂼓�h�y
+8vv���˙�ډ��~�P����٩BR`���E-v^p��Y���M_��e,�zp�h�{Z����"W� �拈��5���+�$bۣZ�!H�,����C�ࢃa��(�.��r���W���������,6l�	��b�=`ʷ�0d��-�}j���P�BM[l)$V{����{!��Va�`� g(Z���m�
+iv>l�reNfǹࣇR��h�?PB��aqћ��D����4�<����C�a�B� Ѷ�'jk(�\���_"n�ASY��֨&�ܛW����'�~�Я �*��9P$Ρ�<�;��28<h^��І׻*�T��W�]�
+��[�*z�{*χZ
+�I4�/�����0�΅��H���<t��k~ ��tL�K�j";1��%o(��j��cK&�`�Dx�;;�.�fBj�J�wΙ�	m�s���q�Մ���{q7��gТ���d�$���,��~����3;��*kF���nΌH�Zo�u�ؓ�r���wQ�G����R43��[���.Z9�"B�{���=���7ZL��b��5��n�x���K������P6�a�iZ @��5n��D׃ْ��]�d,�C�dX��V�U�q?*��Ud�1FC �W~�h���ݨ�ԩ�*�#R;SX��Vj���Q�8>?���G;���� �a�<���9�}*;]���s,�T�/�Of���fP3[�4{�a�v:�;�5�NcI��wwa�N��z� ��b#��hؚ�_�_nh:@���i&Xmoϼ<�p��V��j%����nZ�f�;��VR��7�S,��v��;�P<����Dп�!@�0"+�hYD�-!�@k���S�<�BT@�:J��A�0���[��t� ��$O�E��hӏZ��9��Z���n�IƋL\ыj��w@bF�3g�e8zu圀 �D���}1¬$���MV�9#{:�����b��}�Vݟ!ҧ��eD�H�B9)pp�ªx�P8J��5<Pɇt�(+G��oj����f�eF�Ӭ��}
+�7�K����5jp/>�Gy��5>/�� D�C�/q$��bĦѺG���*
�8C:����r�Ĺ�����V��dp��"&������5��Љ/�կW��F����]����y�4b���f�?�sq�A�c�ef�@�pe�14x�9�S H��8�=�� �L
+	q�����=��tN��bܻ��Gl�G��cS�!E��h������)�FčNI�p�>�S��Ǟ���>j��L\�e<�޹�xFuW��,6���)WN7n��{����z:�>��L�:����
+�,ɥ�:3��k9IB��eeU#K?y)����h��㺨<�{�FU\�D�#����������]����,G�K���X��	!i*@8��v銄��!�)ܿ~�T��o�����]�umF
+�C��Ö\��5�v-��S K�\��΁��9��������oz�P�*�&Q6�ڕM��+�
�9�\�햍3�&������l?�9Ԁ{�B
̌�d��i�Q�4N��A�td9�s�ᘻW]�
�C�Ʀ�A"�Gd�K�m�}i��HN�p)ƴ@�X����gur�C�G:���-L�Z�x ������5�gRr�4w����b��K��oй6��h�JT��看HA��`�됔�	y���nH%���$Mx��%k�r���(�ۆ �u��)"/ۭO��h�e����l�������l�]�G�-��t(�������� O#���ػ�Ynf�`���:�Ԕ�=1�+�{a��p��y0Et︉��w�D����s/-�:��)z��3����!���8�VL�ښ��������{!R\�񌶲T�r�'O���_�_Ŷ����M>jٗ�A����M\���_#I���_%�w�����4�h�Oߏ,����@�P���E=�����t嬨t�8C9�F8��"�(���/�d�w��ۂD�%�X��,�{Z�bOg�v�H���ByT'BOh�&M�E�~H��*eTs�
��WcU��\�v���#�Ȋ�4��bl�"���ж�C�ie
$gT�u���T�AW��
+��f.��{�]F؟�`pPS#��"}��6Q��p����yC�-jaW��ɼ�1��[�L+Cp�*<���(�/N� ��P��W|\,ȿ�&7�9�Z���>?�_ԣ�)�pi!ƉHE$'Y��9^���Ca0-���0��
�b[���f��/,���o��ʈ��$:�N���.y2^��!��Æ/�2�����V�3�����7 ��XK�$���UJ� M��n]�8�c=�/��E�l"�Lq6��HB�0�%�]hM�������s�g���э[H-�J���v�M�9�ƾ�K��TP��b����b�Bhv��^����q�˕Dj����>�w��i?^S'd�:��ꈻ��	1���OC!���J��m�k�b~R ��<�aR��N�<�������`��k��G��	ʋA��7���crw5"��l����ƽC�&�n�
+���10���D�8"�܀�b�#G[Qƞ�l�T�-���
+�%/�KY�*2BN�V�.��$���w�����эk��X�F
+
+�Z-S�K��ʦ�&e����糯ȑR2R�V_��Ց�]�w$�:w�z�G�6���QӜ�-��c��H�`-!�S��=*�V�`'!��ձ_<���C��Ԥ���퓤�3Z!�.�b~��\ǥ�$��ge�g�W�\WH��e��׏�ܗqB��g��JJ8=�)`�	BCIc����� ��C�ԡ_)S��蹥Bv	3� pPh|�rй(^���I�x�@���
+��:�p2�������ƒ�13H��ë��\B�4�MF�	z��:1�)p�r�|�k#�7���lc�E;lI�e��F#!Jۮ�~ l	���er�l��:�Rq��ŕkpW��5����t�)FǍ���>G�8�]��_�(e/�ŕB$���=Co���
+�,q�C��'^�_���1���(6n��n3�`/r��F��%[L3e��)�Z�tڔ��k #��,GS1��'ĴD�diĐƲ
+C�gbׄ�C���ܴ��:*�K#dm1�'���*�잳��D �1CT[`Q�67��;w\�#[��w��P��I|X�kY�W�+W���Λe�<wS������y%&E9�R���'`�s��J=L/�^�]&�����O��a��.o����	��E��x� :Y����\k�������8�r��}A�.lw�#|?��`hUܻ'_E0��8q�*�1�D�
+��;�P���8�m50&Ӑ���>=�R�0��w�����8 �>���6�L�r���^��K����^��iɭ�?��"S���Cpg�s�v�B�l"��r��!�顳�1��l}
+�C���M�}K���'�oӴ��m~�I7b�r��Z����\�q�I�/���7X�j\��ņ�'
٣u����y�w{���En�B��0(�O�>�"�⩂�8%�y�&�!E���>�������b��#|��E�_�
+s~(��兇���GLp:A{I�(1��hJ�L���=3���'��zjW� ���� eY^C�:�oT���Xߝ���.�\�M�C.���$��/���Qy���_�,��@�W9���	y%�V�1��*b�ߦQ�<�D��xLV6�(XF�����,���|E�)�q��Y���a�\�`���uѭB���ܑ��B.B����K&��^R�h>*���]>4�0���C+ۻ�v�(5�1S��ɾk��J�ҕNz����T��(ie^E)6(�M]4����!z����!Ԭ!wP�i��ݟQW#}?`�����M������ϧ���$�(�ӝ��Kde�����lz������8��8:��@���8��2�㙕H�!b��K����l�[�i#��3�EZv.�NžT�--�
RY:y-|��D�*g�8i��ف_0&�B�M�/GWAzt'�y�b��;�	x��\�
�>�mAn�tC�z
+0K��%"�HO�dh@�윹�(���'����܅k`��b���^��yej܈��8��$
+�v+�`m�&�O`{j��*]n���@=������l�.@IM��6��-~�r&6�,�0S����ӎU�c��W����1Q(n<׼�8—����J{����U�B���)I�؈�7s�<"��'<ѡ���(X��LU��nYf:�W[�#�Õ.��Bm���`D��
�C���a��<Z�AAϲ�����#5�����N����8�=h�2�:5��@�ԫĶS����h+f�UɈ�p�Έ��
���8�:bbBk��%���e,~�4,�^t����}H��
+�&ZeQ;�I��/S~2��Q�1�S�
+!Ɲ��-Uӳ���
+'�s	{��"4�Mt��a	���;������l�K笵!W�Kc#�?�y�"�@�8g@�ҁ���9|�v�E���w��#S�n;�
+η���w�ȈO�[D`T�Ia�aūVlQ��؍g�����X��d!1]���״����>�F���'��r>�pF*���.���D�(�,?G;6�]t��.[��2&���5A�0g8���V!���w�0�d,�GMo�GX|�3j���u,s��9v;尴p�����e�tQ�^|��A��*�c�d2�K�@3���xiҿ�ڿ{�����0�'�c_3��'�0�ͮ���w5�_88D�!f�	�
+�9���3�bM/�_9u8�"��*`�oG�u3&�!��X���i�"����u>~9>��)����tJ�u{���L-���g�8D���T����W�
+8�r�*"�DŽ�z��g���H��!"�ƒ�x~x��rӈ+p�.�˱�ǑZ�h�
�"��E��
+�11�����%%�2
r�w &�$�M��كc16�igI�R�^�g��&��Ń�Z^�c+��U���<�@'b���
+�&�Y��5	\��?�!&S�*�u��Y`4����x/�3��#ñA.�)�Sy���2v�\�юBѻLA�[���Q�a�F�W)�pj�ҹȜ�|{�ǀ	:
�̴�W9/�/����|Vo��I3��GD��2�Ԁ�2G,�57���6���'�%���0,[Ί�&�^f�	�)�oT�D�
� �Ku
�}W��)oR�ǥ�:j��u�Fq6��K[���ܰE����ڂ[��l@��n�2�{�|f�YPM�EI��V�����]��S��v`+�9�٫���I��#F����$@��
~��C���d1��	:Q�斌���Z#
+���u"�<%�e������)%�a����H"�w!�(��7��g,Q�W|�M�ĦS3j�2�A�z>�27��0�)���V,�[��ˬH}	��74��]�q�'ޝqW��@Ȋ�Ś��y$�i��BP&�,""[�{���mԡk;�Y�0#pN�Hv�f��I��)��=��A�T��7i<��r=
+ä��YA0�'�v�c처b_�7ZG)�K��-c��8�x
+v�0�n��OY�R�#�^t�X�����C~e:�����7�=���K�P7���y���1:eh&���?�����h�2��~��^���C�/�-ɱ�aR����VLںg΁����
�JFQ_�<կV�׀��^K���5��	�y(c�:|�k��*��C�?�ngvjR
��u��Vw���u�������oN
C'+�:=v�׺G+���el�a�.&����u0Ւj�`������o���1���E�:�4�ւG�Bl��SN.eg2�k�#�o�k���ra꽳A^��g��}�6����
&�M��6MG^�үip]F?��a�N�/S�'J���w���;Xz��=�7�&�-H�F���UP)tHW�?�"��_t����Th�8���]�����I��
+��q׷��w��K��z1e,�*\�O��8�>d�BߣAZ<�	�v9�{U��E����&�9\��|V�	���C��x��G�:d#�Àv�\��NО;\C�0��h:Q��W'D�O��pE=D���o�=s��=�o>D�T~����b�N@�1��t����g�q��8�q���%� ��a�9a�Ax2'uB�0�V)ď��BXmӪ�*�9���eN6ϺQ�f�$�x�e"��QD��D͊(ӪE��/�EF�X8I�FT`؈�81�!�<b�8��#��	dA�>Q^�2��&����$�(%A��,%q�M�d'j���h/�,Z��Op)�6iR%�)�s�Į��W�-�-5i�K8��	[NL؜&_���I�YҢ�HmФ�3Q�3���>⚈g���Ll�g���pr�keB�NX�L�2��};!vL "O�2&�=qM1y�O��O��H�}	�!�	�\>0���%��"ik�ߒ���c��F=P���&]�Y
+Ýa
+�m	C:s-a�bi	�vF�s����d����&��b*�Q��#C�2��P��3iW�X���\g�]z�Xd%�+�[%uŒ*��+>��)~E�S2.XTmJ�â�RB�bᒔ� _���,f��c`�z�_����CJ����@�� @�iiq��X�E��$�Ԣ�rqԢ7	�Yg�I���<��Cm�B����DǷ(��@…��E�D�\�$	�u�"��$�좒�d�N�A�H���H�͋�����<�d��m0ݥ/Z�/��"��/�Q`��t\`�vCG���!�9at0H������@���0Q�1�Gb����Pb���嘋��G�,c��#��K�|�@�@�u;�,��؉�tbI���7ג#s��b��+�8b�e�#�_��7R�̐����4Ðd o3ޮi�g*���S5�3��%�-4�Bh|9#JD�3��ѨNFN#�RLj�Ҩ
��k��Hݧ�F��*_�Sc�Q����1�Qu�,�5�"b�F�Y��k�"��트�
QV�ظSE�&�ȉ�QK�k�{)��mx�"�o�#����DЌn��Dv�QI&���Dn�§ѧo�O"��oX�(�8="ƃ�M#���VDJ![D�Lq�"��8Bľ�Dމ����\�T9Z����2�<s8Nq{���!_/�![��9d��
Y��6���ab
i�:ԣ!�Mux�l��u2D���U�;�h�fvtx!&Վ·�'�����
+a:��\�I��f
+�;�
+�.<���X�G�r�x�NB�ɣ����a��o�E��!B� Dz|�[���y�z��=.{���m��=�8���5� c�8D��6216.0HU^$R�V�YZ�S���$i�pV��.#_���&�x���{ ��'�(�CB���4[��k` ���q��
+�ɜ@8�Ҡ$4����j�aDE�/қ�)D2���@� yDe������� �}� �����iס��xp���1
r�0�@�1L!�v��#J�a�@�i8� ŀ)��N�G��	�V�ksBT�E�]Q�N�	J|��au��N	y�~����T1?����"��	B2T?�Bj��ԏ����H�A�vպ0Ǐ&�xi�{5��G�`��
�}��>v��� ���[���
��(������i�v���/�M>�I��>ć�
R<�b!|�{�=��=��v���#y�r��� q���b-"���N�z J�Ķ�z쭜���B����k��!z8�������yx�<�"!&���]R�� oBL&�"u�Þ&D.��x�a<�lB�'lM�����8a�7!5<�n������Bz��ѻ�k������;�(��;Bf�`%,��KA��v�v�Iv�K-;��SRHW�aS�����!=�t�uL��4훐��د�r��pSx
+���#=R�ӱ�Bz��,�)L��#�îB�1:X�)���~���c��_����&�!�B�l�54G渱
+qsi!җ�v/D���X9�b�L����RI�yG"G�N�Y{���B�v{��n�b�)��8*���8����l�N�:�rk����-��C��Ȇq��3;Ě]��!f6� �����tF��Z�8X�
�
C�j���x��m}8\�5y�,x������OhG��8.p��o���72{����*���1w�DL�ݙ���D|�IE�u���eN�*2���2V�V�q�"E�
�Z�=�p.����{n��0����68ˈ�k����X�6�ӈӴ�����6z�yg��%@�l4X��*���F�����հ�I��G0�
>R������5��:�j}�1Y@R�:k(GS�X㢐�I;$ڬ�W�HC50.�`�EI	Rc�H*���$�O��$��4ڥ$�M�ǖD"Ӏ
+�d�%�i�}��+�!��4�4@?�H�=��
� %���)�K4����Cc��ơ��à�BG
���Q'�T�3ޛ%5���sk���QK��3(��9#I^b�3��K��0�����f�&���Ic4��1ihf�Q&�Ȍ~�D>������tb9�_�Ih��e�-M,m%��S����D�&*C�\�2^�I���
+ʀ7�d�e_2��I���#C���إ̐Q��� C�3���d�}����c��I/��sbǨH'���u"5c�Qc��y1T��rc��*������x`b4�N\1xC�܀��y��p��0<�k��x�	z��o�8NL;F��=�G��8:�`���`��
���QMt��*G�w��/L�N����:��}�p�F_��#�ߋ��Ĩ
+C�rF'~Q�غEtb�^�ZH/�zo^�̉q����ɝx1�xQ_N��]4�9q�.��h��v1�9i_]X����Eš袤�F�Y':>,e�7:����r�TE���f\�R �{p�_;��-N��r����aw�I�3͞�}-��S�{}bڢ���cj���k.�ւ8��[-2J�U����P�����9�BQ�NX�hѡ���,�F����[O��l�(9+��0J�daZ���7�ٱ`�Q|D��[Ƃ�F������B����.,jT)�
+O6ZJq`�������2��Wt��^A���>Q�m��d)X*��A�l���+�yJu\�⧔���2m�PqV+z'Z���ъ�RAjV|S��)���Si�UH�JѮ����ٯ
�T!�ULL2XU��
+�`���:���pZ%K�x��4�*��8�^�x�P:X��TPƊ��"���4<+j���2	Th���>ũ�JO���fr���1�J2�ڮ��%���'L�V:�-�+N���W����ȣ(Z�ᡑ)��������X�?�7���Q�G�R�Q��@2�#"Kzt�PUD$�У(�W�{D�Y�=Si���bg��P(–���~cP�BぢLi�(��'�E>?�����2+�'b��{BAii�'x;-�OD_�iaҽ2JK%�ļh��N(-m�_t
��0'D�󧆤�%��H���ק�81��R�ﴈmㆩ%1�D��t':��Ԓ�`-V��+�G�(?�j	F�Z��:-fJ�jjq's�
++א����b�r���F���߄z�E��`�M$�b�I-h�D~�ҁ��5-�&����>?̈́W�e����82��iLx3DL�V&N�_�ۤ����%Z*�9��;.�h1Z[�F�D˧R-1iY"���aޠ���X��E%\�L��R�?z(�Z:�$F�X��$�>-��xK�C��H��$���P�j$6}�������!�Y�"$Z���I�}��
+��#��#rnP�1t�h,1���B3K	�i���W�T�Y�=�f��F��Y�N7�2-dK��y�.Z��J�C��l��p�%&�3�I�iѕ�Z�������+�"��Cdbm�=Nnɍ�-[3*yf%������Ռ(��O�K3BJ]+�"C#6�CfġȥZ������-:�KN���"�D.wx^�����	���\B ��^4���_&���X�*�bF$Î���\�N̈�&����-��]�K�/̶Ј�ɥ�˓K8���Q����{�;x\�F�G��px-�j��ɥ{6�Yr1iR�v].�S4� ̌���G���0�#������a9�.;e��(Bj��wJB^������Or!AE`�.��~��Q�sxi�hB^<k"@ȼx,�g$���bԈ�{�剈��oǗ��qЗN{;�ҶC�_l���� ���!���������B�>`,�:4Z�E�1[�N!����
+��n�z�&������y0UDW��APq˜� ��b�`�Ød��0�-r�;)��=L9	bB#Ĕ
+�e��n �,b,�D.ؓu	�M����#t��'��n��j1���^��@+c̨�\���CS���c�c?�昞��v���CP��}����姜c�}�2���I�>A���s���|H1�<�9^29�o��E�P$�d���J�Lu�`�VZ��,s%Km˼�س�X�ٗ��t�eԠ�aF�y��f��Sf�5y��w<X	i�N��Wkf��`L,�@i��;��f�����r����%�qV;�qgĘ�dy�r�O���u��}�<� �a�:d	�Tf��!�%4hK�OCS�� �@�0�B�g@9��G�^|�����4������U�Ӕ9e�i�����i��"+"�#�51Ł�@ jj�=��88�b�t������
���3��|���ϡ�v�5�������np�5��s�����y,�5r�F�fQm06�І
����3`6~�
E��6�vl�P��X�`�5�/0�q6�X-h�o5@�R
�6��DG^Om�@
S�M��6]�V��Rѐ�6�F���%р���o����grS2�g�]8lg�
n�q�]7�6��vS�f���~2���M��ћޖA�ᣕaP���2P��M;�M)ɀ��&2�7��:m�c���\n�NY��p�[��jY�p<��5އ��1$�G� �q-�P	���T��q�
��k��<qB��'Ÿk:���`�8?����}aƐSi.9��6L�O��3�#�(�*�P��Xq����B���L8��sgלi��z�P*1�"uN������\��o���#�-�������;]�P:���t\��i��2-�<����yQ�Og��ԩ�,�Y��B�X��X`}�B��c*,��u�,�Žq�@�ؑ�
+p.;�\���踪�ʴ�ӳ�r<Y�n�L�]Ý$X�s�$�
+�D��*p��
+��
+U��
+V�S��厷T�(7*ऄ
+J�NSOa�����h
+��_
+V�J�adR�4u�G:���t�;e:
+<9�܄lDA]�i_(̽;�y�-P��;B���>������N�,�8˄�����	�%��k�|���SyB?�c6�
$�AI�sHhy��c�5���U&<w�R;�M�y��'#ҳD�5=f<�9"��	�j=��ǒ��3n����kfB��1nj{Rv�o�yA&��{�d&d�����y5>'b��W>�hi������	���b^��y��Y"��M`
�xPj�#f8���|�t��ϽL@���z:�_+?��<?{^�����������I���.��g	��?�^	���]���?
���>�@��Xu�@�B���@JB�	��J�t~$ _����bHؒ�n@��H�#lH�??�GQ��o�\Q�9�Z�U  n��,\x�uc�صհ�9�y'��"v�����_#����Fj=�(���@�Z���q�r��X#p�X��l��S�Uڬҧ�/��%�6#(�	��շt����(������el���X՞5��Vm[x)Z�Iی��"<�
#Ց?g̋�s4������"�ʩv���"gA]�/�Z/ly^���������:�x�a-�B�D��ly���]3@	�"��ˣM�Ut��eĦ��"u�*�i�]U^ߍO!���p�H�v_ �a�,x�� ��N�
��OE��uR
+
��<^�"�E�e��@TC)����A딊L���EH�ŤR�^�P/�Gݟ�1�\̀Kn�ɻ�fC
+���G*�\b�����%+�T���s�X=A+i�AlybO����B����
+���
+Ƴ��p!Z��a�FR[�sy�8���'[��<z�����t��'S�<�w�1�A���Y5A�˦49�r�A��+�ꤠ\�j��EaU���dq�L³%�[�}�pI�)�RC��7C81�R�����V�E�J��1M�<��K>�J�|\BPN�*��
+4A���{�53���>����=�6��}�T)��u#.�+�ƣK���A���¤5�E����:˴�!h��4����K��"z���&۰��L��ж�rT������4��y
+�jh' D����횠������?n�lA�ΕC?��p���@�~�u���:��]	f��|\7�\B�߭C�}���4S@@�&�����B^��Ӗ�:�¼���@u��ё��P�{���W����n����mx �+�A������S���aY�:�ut�v�dV��
#�E���]t�hZ��n������9 �p%,^ҀXTш��)��u6m\0or�C��� [f�q��l�S���v� ��_��B�i�NU�n��:���>u��r�W�
W�L���
N�����^��O
r
+
+�ߥP{�
��jd$��y�N�m��� ri�z���_���k���d�^lЎ�o�
"��ב�ufk��:��'�r�j������Nj0+,��}�@W�3��������n�V��6f+�a����^׬��.�V; (x�y�|j��Y3�@�=��?\�v�<���4כ��5MꞐ����u
+�"BR2����K�%��$_��2�1������t1 &vcn�Ow���INKal-J�a�3����3�0H�h�_G�0@>d���Z��m1�����㼸r�Ղ�+�v�_��=x�_�[������cL�D/G�3�/P�}=��:���@_�,��Oٹ��5�?�ȹ@&��Q��X\��=�|}�
{��/�9iܾi�Ղ>�iqp�rGтM�9���u
+|���]�},�Y�0��3ò�G��`�p_��[V9OA�
+H�qf?w�ֶ���~��Y}��q�I��"�ؕ_I��T���+z�/��I�G�
+����
+�>K5PA�L`	��@L`�.@�MV�=(KWIﯫ�	u�i�m�� kV�&
+��k�QfX�d
+�H��,�QP`�m=�_��{��Vl��KE����N@�~w�.�Js����p��W7���t̨	�f�<���2AF^��	h��l�	B����a�C��ŭ9R�%X��ǜ��,�~�#O�}J�㡾3�Hx�<�&��YIDt�� /']$�̒�*XA�t�NA�ƟGpT$���������0�A�ù���ӈ���|/�׈b�P��R��0��A��9�I
"���ԐQL��܏��كS���󨃙M!X]|�[^�YA�6�[�H zEnZ���9$���;`-��Y���e*
+l�sڻ'0e����y3�����4"���Y�.?`��:�W���8i���xY�����F�}E��`
+J����ga�ԋ���a(U���Уw�i�5h ���i
+�%��Ձ 'n�u":0�B���V7��9 ��a$VEў!|Y_ʉ��h`�ШЅ���0RÛ�
�p�m��ݲ�%\E�
����E�����@
�W��]���\RZ	�����|%㢑�kN�$��bi`�����y�1�Ÿ�*,a�f�y��Tj�}����rd�)�{y���Ic �X�+����¡0��Qms��ay�]�|�f�"�t�4~�t�,l�79�f��věq�M�n��G-@��^�]B/��hH�BN�D��+���cBdt�t���R>k7	����N������J6��<���qF��$޶w�F��M�yP�07к2ѴI,�ݱ },o�ױH0;���d�M�h����J�Y����2z�夌p��@����^>	{��¸[8�lbˆ$P��Kw�R�G����Y�a�m�{�[�Ȃ\pbŦ&�[
+?�ym�a
����m�at.D�
+s��ީaw��A �	>��< ��}}gi˄�P�S�F�����8E����M��8���
g:IXQ������ې��q	�׭�~��Q������=�8`�+ހ0bq6`H�a��Q�`�|��)�i�eÝ��蔍P��k�vp{�>dD1�'l(1 	�n^�'�L��/�0_�[���;f�� �r~}����lֈ�>��������Y���\�1�U���Qɰ������/��v4�$e]���R+ ���+���*U\�C
+��m�����^��b(Q5�~,v6(�(G����'�W��_��'�Q�يY�'��\�����J)�V�>@h����M�l���u��q�	@�A֋ ̜��l��#6�J�&����o�$�&����!]2�j4F�T���	�rFLt$�ΖY	���{	�����:�S�	��L�+p�1`Ds�g����`'N��C]�c�Q�a���F��P�N;����S\�$jm!0���v|��/B�	�]<��E_��_�B���,p*&#+�!?��F=_�~����I
+В��� +�sA�@a�QQY"8�)�tG���}�������1�\և���������OR���
+�ցX'"9�;��]ڏ��������d�X҅KX�]wx��X�#i�ma��9����)��v��5�)��JT���x�	 3%N������������]������ʸ���u�͒��"סZ����~&�V�� HG�(i���P��Hnߏ7I�x�R�����j%N�b!����;뉁�@[P&��(ə���AM���\��X�s�V08�����
+x�j�o#ٹ�T�0���
���ez��D�4`L���%�����ʇ��_��E��Sq�����tڱ��Scǽ����(�e0�[�?/1sԔ/���3������xO��������I���B��SK}ڔ�*l�?j����:����	�	���_;޿^T��?�}i[�ʿ�Ȟ]ƿ����	�^���a���E�?$Gdֵ^"q����ϊ	��^
�2�>/��i���l�ژV��m��3]s����Iz�(������Q_H�t�ݘ��"��G�_���|����ç����ё�����7Z���A�����bN��+Ȅ5ق���"4I��S:�����z}����`x��M�c��M���
+'[����ٔ��h�_0�蚩���[i�򰥲9֮�|
D���Z3�����w��1��L#P����b7�|{�S(�I
+^՝��G?�/�d��^H�Y�>��K4	�X:���]4Yeך?�K�Xm�R�?���ҭ1
��_�
+d�W�� P~meD�z��J(?��-׾&����.w�,��P̒�_`k�Cݒ��[^�"��RJ�8�����ד�'WļEU�;�W�U��.	��e�Vz��>&�Q�^sE�捿�S�������o���yO
+�zM�v��Vѳ�1y���$O�m������i�^�d%���/Pvv����U�L`��f�?
+*����9L
� �e���Y#)����.o��.W����#��A(�4��Y��p`�V�fQ�>+���B�0�<�jEd�v���E�~��.{zw���������$��7�� o�_k�ӈz����\5�~0�[��&���k
+���2%l'�U����}�L��)�X1]A� �l�[���
��x�ͷ������}r����B2��]Ԡ�F����P�pZ�?%����`�F����)����y�N׫���f������\�n*�4o�е�Sn}t��G�x�(�!Hî�j�$&�േ�����8t3����R�&l�$�g�4-�e�|/�ӗ�Mc=�@O�䗾����@,
+�I�|=�݋�`���:����6�%VU?lo���J���	}-|錦[4L����cy<_)��T-���d8�/^r䁣C��y�'�����|2�^�X�|Z�a� M7�C��R�3o�Ӷ�XXb�9�y��#���W:�(���,R��YeȔ�ʿ��A�s�d.�0%[�J`L�Pv����vDM(	Kv)!�(���,7FL{|c(�S	(��uNcu�u��Ш��)��2�x#D����yf�+>���|�EQ�>Fm���R��rw<Ǵ<|����t?M�ժ�Oi
+���:B�����$����X{ ��K)M�2��I�����M���S���y?v��0/�~���}�}���j�!v4��X7�_D�[X����ܤɪ��X����׾�������f[��޿�
����2� �����STGH�y�]���n-��E�t?wh��� t�}j����M0�'&/}�ǔW�?�G h�:?��q'�*�_�����j�[*��I�ë��7m/�R΃�^s�S0��ڷگ�q��ή����.�'�N��>�h��h
+J��}$�\,׿�P�D1��Ǻ�"���4Ũ-�`L�aBM����۞��_�����ҷ����"�2X�|�}Jd���[��H˿~�]��^�H��
+��-�#����hׇx��;�^�z���m�A,��&�i��-ٓ�"e}��Gԟo�����'~W�z�3�e:'�y�[��՚W}^	�DlI,տ3���y�/E�~f�h�	Q�zu)��j��a�G�O�)��kԇ^u����^�z�k�h��1]*���?@r�h��G��S;>���&�G�s-G���ү臤v�㎊d'}��x#�H?1ē�0̌z�=�r4×�;�Q�����^�K�C��WK�?:G!&���C���7`�?�#�䶸����¿�LL��@s��
h�:0C�%�>���T�-7NΧ�ڂ<�m�3���q[��Z�Vr�8���V�H�����p5o�������li��?������؆:��b��)j|���F@�̯����7�<�P��)9��0�Di��e�����k֢rk�~��-S�>7��M�P����">�Xn�!47M$��gok��c}�#�_�W_���@�M��������]~$�>WC�&_e�������-yk
��b�X�	ǂ�we>�m�X��))��jB�%�.�S|L�NB>E02�ֱ�
�'L`�� /�(������/�O��xǟ�%RWl>�=�+�*X�Z�J���Ě�m*qk�`�>wƏ��R��oc�%]�/�}�6�-P��5�J͢�[`3mC�3�x���:AܣxȞv8@�����BJ��_�a�A�	ŵ�.�A�)J��s��,�=��_������A|^��`�;��Xh�k�q�-�������,
_@�dD�����.�iW�~��J9������g����sp����hf��d �v�w��̈��Ѣ���
|�%����-�'9'�CN#��Px;��M��J9��aW&���B}NC��~74`TU��y��2"�ۄ�ES���pz�~C��m���
+$����A=�J��s��}@_�ޙ�K�/�����-[�U���`��r�����5�+����S��8Vw��r�a�S&`�l��f]�/�����"��H��ʛ����^
+���i��R��L��	�Ȧ�t�A|X�n.-�Sh�9�{b*j�`���rg+B������V�%r�ڗ�jYl�}[5N��q�5�Aw�,�]�i�r��m�S4c�����Oº�x�`���Z�v�,*�v{H���Ln�1��q�m{�Ȣ��^�$m�t�����|��O�s5���ۡ��d2F}k�&NhS;��]�������P>��~j��I!)���l��~��to`�7�_�T	`8�.r�a}��J���N����?���V�Ic��^/�b}�E����K�%�W�:ӎs��V�J�����|0؝�r�W�\c��)䷟�k��KJnR8�p���fDa���XXns�u}�������
+���7m�^OL����#����M�iw)SO��6�Jƣ��%חjF	/�����!d�y'̀�փ��Vf�j�,��@E�����1�k\z|x��>����?֟.o�a=
�O_�)	G��]=~O�F��;V��]_BӰz��yJˆU��C��0���w���p�E���|��~��j���n����AC���V���r�A�Է���E�J-�
܃)�̞��]'�����4[k���iPOnw����t!���q.�coTFԞ���G~Lo?���������Y?,�~:��5�k�A=\��)�0����f�ԉ����F�������R��N�MN��}eڿ�`��o�ԃ�b��K4���zj��Q��-gG~ZǕك����ˇ֍ԅEy�T�����wK�[?���3Z��[3뱃�~�0�7���ݩ�#wP'����ݬo`�۰�T�A=�!�D�!����BaܣT=�Em=��;y�!�TO����z����3�����aO�����0r#U|�[6}e(=4�H��7�����P�1��7x�&#+�W��W��g/|���GnY!�=G�˰8�/QMz�/`�4�Oc��J�
+1�.�N�]��!��_�W��K�}�e_v�/I�	$��]����p?��dt8ʖ�E��7i��\�Q�m�=].�vHt>ɤ1����������7��M�|���]k�MT��z�j�WIM�(B��X��U=���L,=�nĦz��y"���s��߫}������x�>����<�c�Mli,�\��@�g#l�!e9��$��I=>#���/{���J���1�v��m��x��s��[�`Pzu	��94|�k�c�9W������A���#�R�8�ٍ���7��?g����*X�H�F�������V�ѐ�ť򺸋d~k��G[s�=M���K�߈>����H��
]h������o�
+��X�-GB(���f��d��/5ʹ�}�<��v-:���2�f�0�XC���̯����r�n�U���Fb~��;�`��\"16�ɍ�Q�����7ǿ�U�N_����	`�����������׹�y�UTI����Y��FQ��1��*+����]_��/w�=�U�{#�����J5ӆZ�����b���Mi"}an�"���2�/6ɦj�*]��0��@:�`������� U�>�#�aU�����a~i�4
+"?a�&]w�ys�\����{�#���Tw���`��3.jS�y)O�� �=����U�uT�-�s�����r��G�k6�<|9D1�'Dz<�3}g�9Y���!�zt��.3\bNr���1��E��˘2�|߻e����q�	_ӌyuR�7;\�6c^�����fe����!K/�B�����1�I߁��,U"ܺd��6�A�y��r~w��l0�;���0?VD�<�%�ֈ$�B��sʦ�N�@=s�b�0��k-X�'П6�$&!<�����0Ǥ#�ePg �0h+������K�؁��1f� ��A��1���\�N�w��j��u�c4��$)Ũ����J�c����$�1_�~^�󶌹��Đ��8�o�fS1���&-Ƙ�f�hr�؛���[T�V��[���q	�������f���'
���Y��ˑ����0����lB�9�%YW%6c���TZ�$�:��>澓Aߙ}�k*�1�YҧV��(:�:JY��fk����Q�/���K %TY��ܛ(�f�tw�)g%�Ov'�J �'�ʩL*	�����r�������^�H;ՅͨM��p�ܐɁ}��¯|��?{O�+Y���:!Me�Ir ���� 9�ҭ�!���%���"7��|���?8�H�+�$r������X�ې/F��b��̍�Lu��3��� 4(eĨ��2��t�>��R�JX�6j����禶�H���[��r�r��DI;>7��|t_:�Ga�����29.�nQљ! �84a��%�i�ƃ44�Lu���qJ㊂.uUK��O�-T�ʢN�#RQ���"���͂�F~c"�
+��2l�Z�;��.����=�
yhq�{��Ђ1�_�>�,la�_��_ġ��S��>B�d$��F��:�� �ǎ4eS�2���",��.�ğ'��3U������$�b
+endstream
endobj
19 0 obj
<</Length 65536>>stream
+���T]>�F��
+7����s{�4u�w��7��m�����z-�B�4�qoؒ�-�R�s��@���=�����#p�,����+�\,��h����
+qd!�{�w���8�;�ʄ��p	\v)_S0���f�8�Mi��}(X�{�� _���>�ҞpM�OEou�ߢmi^J8�@�g�F+n��vS��\.�����<8>ӻ΄D���9��^�0�,�^Ǽ�����t��+��� &�#-R�+P>-a���n�祮�|C�V�HV��Vu�3����h2ޏ�vD�E��C��}�]������J;�{�>����*[ܢn*GL��
�Y4�c.	���Ӷ�}��)V��2����lt�B��A�]����gc|������27Uz���6�Go���^)~���
+��'Z��]�E�n��?�J)u�o�^�����G��I�;^�M��g�\�
+��"='�7C{��l¸19��ZWu����{��1���{�}ʉ��=����7��s
+������������M�?�w�*��*�k�������MQ��&+[F���*q-�Ị���^*.F
`�z+Շ7�]�8��������[iI�BI��E�n.�h[cw?MX�zٸ�Kt�&��UE��&ږC�݈�adR������^��ݟ��j#`������L��u/4Q���M�ϚC�n�g]��҈�,�c�n0��B��n��F��ujk@w�;w���e:*7�EB�� �̝��'��>�P,�F�'Ζ���f�$��W�P1ӑ��@n_^��o�r��ƃP�W�*��=�$qw�2{��z�(�P���g��Kp;��9~�7���z�u\(���^n�_r�b�{;)y�A�RS�N��A����r4Õx�}_���#WZ������N���V���E�\�+�w�IV��]��LQqAWH��(�K���B
+�� �6�v���(<'����W�/$�U�mo�V#�~/�������"K��Cb[��74f��#{��һ6>+�VF�����h�d��h��jg�
+��	��r�aM�ӡy�´�R�E�ۋ?LڂLqgKW�;�2�Qv�q
+E�DQ����ж7$�%�mB5,#��z#i�8�g�� ���wv�4_�"�@l��MUb����4��q
+5�I8��e��V����$����ⴃ;̎�I �/�����vR/D��^|Ų��8X��ʎpG��]�'k�U��������E��x^d'� ��i������_v��9+�ƶΚG$b�C&��5f�^�!�\l&N�7a��{�5Uȱ��1~XF�];E'Q�����9������2U��i���zSG*Uv#�v��솒$X�W���.�ln�
+�4ˉۂ��mqˈ}��][T�%��&�Q�{:���Y�״@�r~TO�˯E�#٬0M����(�[�^'w��Ĭ�.�Q�����4��$^*/��]�X�2lO��`*�Х\kQ�t>���|t-`�襣���%�PK��q]pD��D]����!Ji���|�J~��z�_�3����x�5�����b�����Ok}��qZ��a�(ĩ���i��΃�0&/I���sp̬Y��pC�����^|t!k���=O&����'u��Z�a�54W;km�X���
+^�gQ_c��Q�����f��kD�X��!�b�B�/ik�u��g@F��b�-D�iZT���3'��_��^�`
��Ft���5�2�t'惵�~��#��?X��Z�
+u�;X����s�o�2?X�A��It����~�ŕ�z[��f"�p��h��k�sV�`���Kˇ�5]�l���j��XrJG�B��b��*6	�90�4D����)�ƚ8�%�.
+�r8�
+��k���`��H���J;�`�, ��6i���4?�uoI�xL�n�F	�a0K��(�PS΂�7t�Q*��w���Q�]ב�~hC�8ǫ�X�$������)X��R�Ie~�aw��a�P��:����(���t�
+'��A������$y��թ]�LF��Y��f�V� O.�[�a��K]5��o<�w�UO���%�wP�Q��u4���l��2�&ذT����JΑ�ҨӋ�Q}n�2��ݬ
G�jvt�?HO� �pMY���Q�ڀ�
�M
?�M ��"_j��'��KI�q-u�E���
+*q�]H�(���iG�7XQ`/�}�@i��b]`_�I	�Q
+�����ꁍ�!zS�Ǒ�y::�>ܚ<z�C�U{��-y)H�P��T�����@q���Ma˹)��[�YeR��[
+b�ϑr(��;�v&�40�bV���!Z~�v��,��V��F?ʍK�k���1i���4��=KS�-�'ʴdV����4ߦi�{M'�~[���6��B[�S
+g�S�V�W^���
�-qXܱ�E�f
+
+y0�Ta�`��ю�Cvsʍ	%���NQuy�N�"�3A��X�#j�m���ݰ��JY-V�"p��J_I�I���#,��Yf��~�������S9h�ӧJ
+�#�F��$�*���vT�͗��x2�ڭԩ���Ç���yV��j�ȼ���
+kpaJ�Q)�m����@p��P�^�J�q�zW���*��{X���V����GL��
+=��>�~�!NJr�oC�1�Bv�X�jٛu��h��|�Dk�z%�B �z�8��!��1&[;��֢C�[�����}շ-�V3c��39D�t�/͵��GW�f`�YM썘��z�-�+���?�+����4p�^1V������)g]}e�wM�W�!���+�#�b��;�I����*�!��`������cY��ϛaQ�2�G6���r	։� R�b����0�Eh�r5ᡆ̱!tM8�c?�/�ؑ-�mǽd	؄iP�4�Ke�m\�.'d�^F�k�������l2�9%�ef�2����b��UN<�����D8[���Y�:/?�,I�І�{,�;Y��^�sD�K�V����})��eZ�;n�N�h�O���ւϩ-(�TWm�؀�j�{��m�DCm����`+��b4ƶ���ve�>�$�t�i�����$��A��n�P{�C��5���'������+���r��ѹ=0D��㾝� 656ey.���\$��p��=�҆�����Z���Nf��[�Fg�g�x�d�iR�	��n˕L�2w_��O�\�[��\�]0q@�{�J���.���H�VM`��ĨۉD����E�ֈ�z]Ϋ�$%��5��t��.�IT��q	���m��n�g����E�������A��؁�o�����J�K��t�Z^�V�{6oA��ڍ�N�ރ
+��^�YoY��b/'�^��j��Myl�v���
_��7���$�����}�0��8>��CN����w�N�Q̯&�ocO�f߯����n�2��^�����.D
t=+�[w �xkEa`ȴ���՚>Q��ہe>
+�6��c`��[1��̜�c�L�G�҄�����h�7��t.
+wԯ<�q�MY���}S��s�ג~�žVx�ըZ���/����Es�o�]G���
�ÈL3�r��g�>G��#��[@�sa�@2t	�Su#.�2�m#ƣ��V4��$I�H��no�q�&F.@1!�5tp5o,��xi�X��N�Sr
<��J��u�xw��u1*�5��p\bi��2N��Qr>�KY�#�5���:!5����ɘV�ߍ-�c!�lt���ԡ�Y0�1g����㫆��<����{�1~<��z�R�'�b��y9��c��.=�	.A.(���0lX&ȋ���	�0��ge�O�H����Ú�I�����w�*��`�
w	9� /���"�wG99
� ��_����RI�[�d1�1E6�cd6�Z�x:���8�#'P@��Oy�RV�}��G��l�<��X�`�@#L)h�ǒ���+�P��|�'��{�F~l�Ϗ��rȏG�W�y�>�J�i22��{L��j��k;T�';�̢�q��f�t-cS���X=[y��q`�����-g��q��#>��C�$b�����B��e�<��o�2�Cg��_ƣ���V�k��=s�Io���E1&Ɣ�X�~B��vç�$eo;6��p��;�~@v=��{�{������gkR��ӂ�ģ0�a�8�"�=���]a�E!����1����3A���]TP����-�SoeR�Eϟ�H�E�A�<�������<�JQ�q�<��3t1��1v���,j/��+�/��q���پѮ���X�U�
�$`���?�N�m���+S�hԾ�a�Q��q�P�|����c���t]�ύD�[�����d�j�Ԗ�n9��)��n��o��K�$� �sV�K>!�dƯ�ϕ�Z���%;T�O���8_ã�I�u7Ker����P�O��u1'����&���	�o�S������;P8�a�nKA��{��)��3���#%F��UA���6��=	y9}��FV��t)/�<�]�M_��y[�Q� �t�5)!��L	�v:<��d율�ςT�H�ujD�(���MBKn*��m�[�l��BΨJ�YA�c�)J�iY˰��Zۨ�%��UI*d���]�j�g�2W�����B9�1W�ra!�<��y�!�)Gq�.w9I�82d���ѐFWzB+א�Mk�b��V��А�́��2Y��5�\�\��!+gq���!���?4�u|P�[^C������'C��8��!��Ƃ-gB�F
�n("��銌��(u�I6	��|���aFd^��]"�ɶ�~qr�ؿ��2����9+��D^"�וhp�$2[>��mD�����*dYsD>���U�"�!��ƀU�"���O�,���H��$�u�*���c����`Y��F�9K�8mr`�LN5r���F��
+L�\������F�'i�������z8�����OK��,C5&�V�l��J�P���O�f��
i���|�I<lc;J��'��G��`x�A4	#�#2ZN%D��%/mF#e���d�H9���	kv#�g�O��)�'�#��q�c{�P�1RFy�煙r�&�ҩ�4Sf&�v�{�	a]��Z�H��O��9��o[P����5X5�|͔��$@+�L@�FBe���ʜge/T�\dj�X��2�dH6��S�ez�|Ė�. ������rU�sy�5?�ܾ<�)�<��>~��8�5U̟*��ܘ�Ê��ف�,u�YX�ml�ؐ�̚
�th���i�d��K ��_���k�p��9���͍m�%��u��M�-������Y���|qΞ�ƕh��f�n�!��zrv�2�8�Ei�L�������v��h>�3[KI�#��I��u�]�u� �Ѽ�\�K�}6��)])��X�Ԁ��\
+�t_z�Ơ�Ȅ�*��[�e芊�j;t.#�%��(A����uEC�'���h�� it�7,�X[�q���OK=Z�n	i�],��H�’^"
�?i`�Y�>��럧��4QE��!��(c�+�>,E&L��e��R���R��[�gN���5:o�t��p��|A�1>��]T1z|q�k�EPgq�F�:�1�~����c@]���!���z�V�S��i5
�	�h�*i��a���r�7��Pc�/�k$�Dq��J���;���~���=�t�"U��$�^��閁)q��\
�,��#u:6en�H]~�k��u��nb�)���O)��R`���	��4Jj:n�ղ����U��`#,���O�BRc~WX��S_�)t��N���*H��I?lzHR(.��P��x�(>"�+`�F-۫��zҨ	�`i�g
A�=�����Le��i��:��F+�m�iF�D
+45�����J�l�,倐xi_! �Q�%HK���!j��܁#�p��O5&'���f�.�3D�r ����B�<5wg��$\ԣ�k[HȤ��DLi����+�i� `7��5~ԭ�(m��v?Ku搼eP[KAC4
+�����Th��VM����P?���j5�-)Yt`Ԩ[LP �ԍF�8څ�4ۏ-�ֈ�2N��Or�Q3��A��.n|�j5�3X�iU�f�F��{�j�H�S�d�c$0�����Y��8ģܾBT�v�if�wlN��r�[�����^���1�(��0����C��P/����p1㶞q���	��j�L$��[3�ۅ�.=�/�M��5�GY�5�P�^ׅͿ���"گ7&k�����6*�"1lL�{;D��i�&�����_���=6�6����Pz�#1���f��N���|�lR�<��@�淶�!^>��B;Z��Ѯ��	�$���3���/�vX�R�is^��@��[���0F�w����܈�niY��lN�k{5Կ}���&�n�:��$t['b#��'Y��͋\T Wo���U� �_��'/�}���宸��>g��Cs,%��ʌF�!��ro�/����zb���t�!�6�ݐ�t��=���ݯ��/���z!лjk��zc����o�v��ED����{�|_[�ד׷���8�Ef�,���2*|����6*��%FϿ�L�]�Ҧ;�-��w���\#x���8�B\ބX�\��K�`��<��,��B�����x��>�5���g�B)�`�*p�[*��}Ax����A��𹷕,8&[wbZ1�4�f�G<e3Y��Y�y�*>�D���)P)p�5g/�T��6�I���"���V���JK�ڰ1�l�xR�Ѹ��dX́_լ��\2��a��ww����ȁC}Ѫ�'v|*z���
+ܾաR0�d�^��.�
+���6�I(r�>>�j�<��U�nC�'�����K��Zp8����db8]�ag
�
+B��-����1�="��HD�9`��.�*B�q�&���+�x>�h�{j���},;��E�0M�P�B����R�ΜW��R�#ʧt��A
��@2���x�P(�\ #)��"��5ŋ�4F$�t����Mq��y�Bz�UT�ʩ�����y��T⏔�3�q�.af�����L[ZhX��"����*�,�Ѿa#�@����Ӣղ�=�N4C������̕����ʿ��~C˘�/�á���m�"���$���4��E����������ó���ϴ�DT��^fd�h����2f*�WEQ�D#�o�E�T�����"�u����vVx����s"X�du
ǚ32�v�^�"�0�CV�d�%��;��2��c��M�x��Q����(���<4Է�4bs�*���������7���!{w�PߵP1�wӽd��&hz����>	3h���7�b��n�4��Z�/������������AC�rhVb��"��8�@�T�c8���z�\	��t��/�
ކ����������URx��-ܣG2��b�Y� �i!gfq㟅�p����
��=����U@��E�R�[?��p�8��
+�E��8�G1әx�g+���%�ĉ3�R��k4qj?
+c��G��u�z�.��h�gi�o���I7��y�b �]�d.��x��]&~��&��d2q��� unw�Ļ~;B{���� 3�Lܸ[QC52qK��E�b�[��(�]�9���i�ղ?U��&^���������ij
+ޒ�yKE%l��1L^���6q�<}�����3���Vm��=L�6y�R�� l��2`�tf����
+����5�-1Î$�q뻊Vϡ��,�AI2IJ�m�����Ib�D׽��J<^^�+=N����1F|>[^�6#^�`��;P#��.���?#��Z�� `�]�3��
+���me�k�#>���ox��qSVs�F(�� �iu�9q��#-\I������8��l�UJzH(�A�IQ_�&Kqa��pD\׋\��'�)1��W�޷�����Î$[+����xu�5�xiA�q<ƸJa���|��c4b���V7��A~����I�����	��~�LlX�*�&��:������aY�<���9�XV�U��qU�Z^gjה.��y�}9�v
sc$�}J�����b�=z4n
+8DBs��6��f���xs�g��R8���	<ϱ�	���U��
+�<�������!4�#@dz�,q@�G@����>k����l�φ�����#4��v{ң�)�R���MN:wf�x�w̱-��=�c:c�u����=]��de2]N����m]5�"�y���tQ��H�����翩^bM8���y�p�U,Ϩ?Z�d]
��֏�Sg[O����i׃�=���=:$L���~�{*��"^b^K��}\�1�-����"zK�e��V��{����"��)�vЩ�Oۙ>nι;�o*�?����í��]�{,�G�d������˽]��Z�O��,��J��I�7����o蝧��y����_�s4d���8���k!N�T�`�K7���
k�?�p�5�_q�"y�?�!���o�!���Wb�f����
�x�F���F8~���y<�wC<��r� �_䇢�[���N^��.;y��\�hʏ�W�L�1���%�a�|��n�RC�D�M�3��<���Y��B^�<m@����ʊ�/&қ��}�x�?="��Kz�/U.��C4���ހ����
����B���Q�kљ:$�k�f��F�{[o�>z-�ד��A�sľ!�"�v�OB5����c���>Y�ך��=�am��*ܾ}���DW��k����QNzv?^Ӷx$�{��#@�ѓǹ�{������TVk�<q�����1|�t
���j�93�Q���OQ콊|޺z�|��˱�ݛᕚ���r>��$)������s}�G߸$L��p'����u�Y��v���*�����/�:�%������(a���<�A�R��g<�o0~�!�	��U������)|%�`7?�]GM�LF����?z{4�X7U9}H�X��������p����;��Zt	�����qi>�O�,{�0?�P\�2�7n�?�v2P@��8d�@�?#\��3�זI���^���e����:����?�p{,�������w���	����$lV�c��	�$�{���QRy�o���m�B�N����?vO�����/H�?,}ʁ�]д�ȿ�t��Ug]��/�UlQL���T��"d�La��������Op1l������:��@�]�[����E(�l�v
�0rM��H�}1
+@��6��X�2��"�m�n��L;�Y�^��[w�ꋼ��!#!���H�9o�l&�A��&
+����U��'�h)���NIG�hJ)X���b�<:�0��6��
�NP���y@��i���,�Β�$&\5�g������$�=೓�[׍{�����b2.���a
+�'�XL�66By��Y�\��B���V��b����4�o2P��$�����f���@<o
���5���\@��o@�hV�
*W-E��܁!�/���$�Tӎq���)Ye�1$���S�<�Oʔ��cNB(MR�~��a��)�z8����^�����&�����O�˱�(>v+3��
H�[K���ߪR�i`�KKf�ޱ[��aAB�>�g�O�]�h�B77w��1n$��T��4m}�li
1ӿK�!����j{J��h~=K��|Mʛ�)��Y4@�׷pz�����X��M!_�w���H� 7�� ���O
n�9I[[F�2q��A/��4����"UD�����#q3j��+��/�u
�CfOsqVvK��a@�>cؒܔ_
+*
+���Ӆ�ױem8;�<k�坩��:��⌬Y�����">����������������;<_�������GYIg�������w�݅��l����������!<##fs�c_Gp��&���3vn٘9[��_�C��K�Nw�pu�c9߸���n�	t�c�Z��7��9��\���2M����y)�E�!	���=�|5���,cw6����6~m���.��q��> ]����R<�
WN�C�^d)ثpP�\VDiXD���Q��,�A�+m~��Î@��R=��B�4wj�
 �/����(��2se��]Y�CHg���#�����oYe�Z��{�z7���=��{�b�Z�4-�U�+��?$!����G�������)iju�u�f/���K���h�a����9K�8�@�z]�[�D��l`�V@�G��<�t7Ο����}��>����`�ً�쳡Fo)S�[�=��M�a���`��8
{�H?���;ܼ�3H�o��~�^[��{ܳy�x~o�'|w� ������h��h&Үw��3th�l���]�ü���`\�%���)�+��f��t�\PL�	C:i&P���rЦ���)ᬧJ(~�N.h��Aiji��h<2z',��,�N��[�E�{F���m�1sۙ�O���3���ؚ@�y��n��y-���`ֲa3}j�ƻ�H��HW�{�$_���3rm}�Kga�N�w���Uq�Uet����k���s��������Z0z(������>��B�}��$�հ�������p�YH4e/;�(����H��߁�z�OZK�&�@����
��?��^��_���H�o�$?(��\�4~j��$�Vi�kC�n�&mn޿��4yn�M�\�Å�9�r���"�>MT�%�Ct�73��u�q[�D;�Э�ܥ�9\95 ;�z�Ӏ�a_����Kn�1��r���h�R<z�N.�����y���	�}����M�w
+`�ءHv��.�[���uD���	h�|��A�G0���uB������NX�]��������c�vJ�w���3q��hb�����e�3X�}�>��^��>��q���1��@�{vd�
��W�h2�������<�g&�|�j�7���Mr��O�aۉ�k��諛����1C��I���x��H(v';��A�E+�tW�hk�joX�D����o��G�aٙ<�I��?g}�h���~�B�M��7~p�\w;}n�H�{�x6V�Mm��
\Re
+H>e)�_h�ϳx��p�������T@��P�j��4|k5M�SWC�~
���KI�o��3{g�L^�<T���H�����_���������E�������9���3��v}�ǘN�]���,�����U
+~���~U��9�8;�s��!��4|j�e_o�#���%t�j��|�&k%ί'��I��/��ɵ�P=�I��&���dm��P�[G����gK�&zȯ����`�
`<�'ҰN��9�� �>���<�t�G1����Η��Y��A�}h1��9ijo�|̝=��}yFoM��K��ݺT�Fz�ǧ���Zn=�Ŗ��.#�>J5�-�8E�S���g0϶�s�J=�P����c�m���c��j�p���ٵ
�zɒ2o�Ż��ޝÅ�6~7�q��3�?������/P���VHv���1ӎ�D��+ ���O�����t��A�|��R�/p�XC�QI3��XG�aIH���ż����JS��v�/&���^�����7~p���t�@�x[!H�����SlN����W<0����hP
+G����ʡIG�QY3u~q�o>��L+sW6���|��_�ϟ���D��3{is����	��G��_A�HZ�'
��h#ev=���w2��N"��I�����ݙ�&PM�y���X��X;e����B��=�>s�6?����S�_��V�ܝ�>�<�I��p�����~l�Z@6g-�Ө�WP�	� G���ו8	��D;
+����8�_ȱ��,���"���Zh?��o Ŀϓ�6����k+mr���#JB�I��v
+-��,o �=��g9��G��wQ$���)xA�y�H<ϓH�e�д4~f�
w��x״7�q����wВ:o��� �����F�����u�v��0��p�~�[�!��F���@�a�R=��,�n#X�x3qv�7�l�.����Z�������s�r>fn���;�Q�y��=�Q�Q
+��M����ez�.\��[��0�������$�����9�9��q����&��a��i��蚾7�'q�9�}#��!J�B�GF����7�p4g�L=CwF��}@�>�UTk5SK5�S�xT��v!I��4��R<z*�5�Io$��;�u=�;ב.�,�^A	�LA�'�e��_��N���'Q����@�{�S�a������DT~�ò>��B�}�ɴ��:rYc�J/p�؁���ݙ|�u��D�SjaM�"�O�Pl�*;S���pﺏ!_-��w�(�� G�.$	XO�h����T,$����?UB�S���N�wO8��p�<�b��$��I?�^*5Q�����>ϡ�7��Ի� �/���sZ��7~r�&ϭ���>��r�6`Zh'�x'�x5T{o#�@�����8�X
{����2�����7;{���b�ʲC��4.�Z�/'f/H�Y?:e�ϰ���{
+�~'�_���>�=�3(� ���_��EۈR����F��
�n���YF�l����6�i�G�����+Z�����>�v�𭖱#�e�δ:_�3G��j�:_�׉.���e��A��4�h��ݙLӇF��������}�<�u=�KW����C�~;޺�C��u�sg0������Z&�l��K�m���:[˷ Bp�W���/�إ�\��㲦���`�bL:��='7A
+Fa��Np"�V��rm+�����נ�c`�ٷX1k/?�$P��D��bL����[C4i���o�F���F���j�d��I+X�؋0�~���\�6�x��!�7p�nHv- �3וSm�*��)��1��u�?m�D�����[���׵�p����H�&��ɿۈ��&���&	�e�n�,��:�����F,�&?�g�:�s�~
[�sh'��g�ホw�'��w�vv
ޚ��k�9ܹ��g7uv�J���Sh�-�zx;�z�$1�'q����1te�ļ��H�BQY��ۯl˷ٰ���VLWUv+"�3�����]P�!���#���w�wh��C�&�+���eg-d���L�������y��<{',C��k���=�z�/M�L+cG&����3wisLܗc�|m��W�>6�g&���7ܶZ'LW���2w����8YV��ڱ�:[�l�k���7���7��$N��IĻ�}mL�V��S�<�u�ׯk]{=��W�;;�w��ژ�;;�W����,���aHҏ)�k+ur�&�<������=�y�a�Q��=�vN���@i�v2��K��vd��i��@�z����m�I���2�6�qt�\��n.��M�������u>7 H�z����s�m�z�a�G�$�	P,z
QN�1)"��+���
Jº���P
��*U�A�y�p���zܳ�(�*��P���Aܳ� �l"I?[�����u�t~�n���k�6�o�[�q�rux�����D@(���飳u��H����w��ɽ��\�����}B;�c��s����<s�&=�u:$�U1��xW��}L�NM;d���I��R
�-*��.��zqI�) 	��"	m�n'�r�%�t���'Q���=�x�Ư����yA���}�w�|M_[��#����fڱk3�Ǯq_LF�l���6]6ZF�c�~���W9[l&OMK��&�,��7ܷZ�.M+S�&����2ve�
���9��=�w]���#uz. ����0���C����f�O�����k�-_�l-[�^��=��&����0frL�<æb��=�x=�[G�8���I?���k��f?5�f������A�~�w�w5P����A�}]�K���d��\}�i��F����7�o5
��G���K����F�A����<�t~F/���3�Q���N�^h���t��M����c�i��j�]����<a	�vkI�l���O�;������G���$�������u �=o$�D��Gq���~����o�< ܿ��8rަ�
�1�U
+�D����f�������5ym��Q�U�J��w�c����7�q�'�Q��t�|�!^7��L�]{h��|�:
�����D����aW���%P�g Cr���S���S�JU��s�ZK(�)A����1�Z����x�|�d�ziju�tu
�Z}u�f�m��^�X�6yѮ���8��׳0m��_�vg�n>��<�5�'�����(����Hs�~R�0�(�>(ȑ�Q��E=�'�O�H��t�W?*k.y����Y3G�����1s7�*�Ǯo5��fca���[��Y����@�z�����Ф���[��Еeo�nt�7��|��w�9���J�����_;_��F���<�u��j؞:��(kE�΃x�q���`��!d�<�w��'�g��7��A�>c�&����7�q�%�?���
��{����������^����N$�P0��Knj��=�1�5�}#������ă�)���JU�XD�`T�V<�M��h���x�<�`���S�i�ֺ�c��@��:=�D��v��_���S$}��B�}�N����x�<"��Y��<ế��>�w(��6~ E��㭳s�s>�P���y!I��cx�s�q~�k�$=�d�7��J����齟H�_A
+Fuz���웾8��φ*=�l�k$�E�_���c��j�p���ٵ�N�H����b�jԻ��ܿl�2wes
[m����Lþi4�Z@~-&}��������y-�b�x���^�	g���b7��<�t_(��+p�X_���А���h�T1}��߽��=�w_�[�s�~���JE�f<z(UD�cx�o��<Ο������������zܳ{�~��ݯ���c���82zL��"m�E�dI��6�އ1���S�i��h��7y�0��I��@�|�q�����E��'q��|��N���I��E��J�X=�E��}|W�p�j�C;���"�l#��/5цPÒV�±7�~� �>�cX���Ҩ�G�&����i4�0{�&�a
+�#N�>���vc�V���O�X�S%�)�ez�Ⱦ����R%��S�m$�F���H<�4	?S��~5�O�a;)���l F>ߣh�u
+�|���'��N���Sh�
���w
+��O"�?���2�6��������ˮ
ejh(
�G��w���ֻ��l�I?�r�6��u�u�F�� ��B�}�(����u�u]�;g��l�ʿ�t9h#i~�ʿOd�g'a�J�]�H��r��>�xD�A�|vW����(�D;IS��[�2tft��=�ؕ�9߷���W�:{]��B�|�(S�Q�y�M���%{"��A3��g���J�?���#H�؛J�_q���S�g��d@����W���f��9���lۛ@��&���k�7�p�&��p�z�\}u���=���y��5O�w�e��h:��������0f4f[gk���I?_���w
+�����f�2uf�η�9���<#W����7�s�N��/����D��� �:�a���c�m���ʿ�4��.�
+�Q�����4� =�P���Ȓ�u�u]�kW/}~}�u���_���N��ϴ齍(	��(�?�aIG�Q��p�T4~�a�H,�� �>�'��;��y�zO8�C�}$M���YxG�~��D�H,���>O���Q��D�}�Ф��a��C��������y��s��m���L�Z��'��n�Ϯ=E"ѦR���h��J�%ͮ��齡J�&�#�@;�sЖ"ahK�n�4	��$�m���m�:Т�_���I�ܟ���A�{6!����R��v����PD;ȱ����7�o^ȑ���}=7��g���I���R���	��W�����e�2��f� ��k�-�>p[���{�<M���}�A�{_'LW���ƺ�SH�2��%��Q��:`�>�W6���rL�V�C(�{���M�F_��1�
���+��ȩ�1ofZ���fp����u�94��vk�k�v�m��[�c輖oA�ೇ׷Z+CgFs,/�����;��s��ȱ�5{l^�+wI��B�/WV��mg��Z��:ܺ�ӭ�7�p����&}e�_��~�M���q�j��cXׇ&��%�"(T�o���G�"~(T����:\��8��8��߭���ɵ�F��@*�H�O�=5B�C��Ib����N?j�g���C��?���=��;�v^�;�w�w�1�jܻ�"��þ�s���(�m���N �юJ5��,��0ߝC(�g��j�?��Q���y��8���<�v�������4xk�Ǝ����}�I�[���> jh'�xCxyO�8��4m��A{��5���F��I���h;���Ă�������5{otM�[
��g5��=�w������8�r^)�p+u���X�D��G0�����4|h���o;��w����9�r6P��=$�g�|߾}e��1u�6'��m���:[s�
�9ˊ����
�[�Å�6�k>��i���[��yl�ݝ�����ʽ~3��m=���`��1�4YL&o��	��:�r��\C�V�ܝi�nw����|+����Y>��P����ja̴c���,���ce�L�V{��ژ:��ٻ��5�9l���0p��y��B��*�g�־L\�Õ���~P#߷�{�>�z���{�����\�#8�}�{~I�{ 5�L������m��k[�^��N�
���Ą�ƚ���q���&Ѯ����u�q���Y��R��v�i�g���L�_�'����9�r���R���X��&���=�ǰ����;�Zf�m�Q��>�y�����8�q^��f��I�]�����}��<T)x+an�$m»��o�����F6Ǵm噺4yό��C�y�x^h��&j����{G�n{D�G����6q~�O���Q���pۃhg�������Nc��o����#�m%c�o��J���
 �Osm$N@���m���=�w�Тߝ�)x9��B�|w&��$꽓<m!ɾ}�j�l�V�;׉&�w�4IH�s��u�rwN�ͦ�[�e�nZf�,{u�r������0e�����b�c׸?v���:a��'2��<�y?����u��}�;��H���fgy��
+��Ƽm��,,fMv��ؙ�3-���c�F�t�d������w1׶���C�g�д1r?{m�b\���^!�M��u9�����3�ž`̰b0kca��Q�îl��u�]��G6�x�l�n��1��i��jƼ)��&	�~��Gs���AO$�:ܺCg�����l�l�Ф_���?�y5ϡ��I؟P��'������n������7�m����BE����=4����{��F�w�v����G�k��j�����gaʴ����'\g���<�y&o���c�i��f�D<�y�c��d�e�5m+�ܩ�=7Oc�V�إ�5zj_�}f;ka��,͟�sh�s�h�<Ĩg���<�-
ޚ��k��"��#I�7���B�w���!��7ogP����u�A9�󵳝@�����$׶�k�8�q�H3���}��Y�gQ�M��ۉ�E���6��R���iiju�svO!����5{nu
�[}t��L��m�#�v�������4~k3�[+"�����W�>Ʊ<��x��~�y��}���ak�jZ�<�������5��c������1ufc3xj�k�-�{�.�����k/W8-��n7�����@�����b3{iX�5-�����pm��eaڴX���8v�<�����A�1�îg>ĕ����b0i�������p>�8;ZXXUVN���68YiU9U9Y9i=pz�V;=p��夅U�t?h����������z
jhn{j��t����L��ܩ��a�$�Q]Ax`�����J�	�ՕV�SU���S�\ΎleA���V�U���UU[Ur� ����jK�j+U�-,-�� `mY=���U��*U `iiQ=��z��
++++++++�*K.k�*--Y dQem]eQE�� �*�*B�YV��4`A��ڊ B�+��D�‚�!�V�WZ�
+���"\YU]A���p�%g�e�*+�J��AV�\��,W���zV�*-,�-�,,�������� `eUe=�����z�E�+�U�,��TVXVX[TVVXVXWUYWXVX�"heAز�ʪ��ª�zP��eE����!�TXZXXZXY[
���������������4���ºʢ��z� ��XW[XYVUXUTXWVT[WTZYVZTWXW[[TXYX\��p>�k�
��>ܩ)*ЭK�E�j��jg�ŧ0�NR�	U0v��a
W��������%��S���2~܃����%f!��JB!<*�<r��=
+s�и,rZ9($��$�!����R1��qx�塏N�G$��&��9R�bfi��/O|#�-_�F��q��s��:�[9,}���)�Ce�>��;A�'.ċ���7��p��w����"r��pNP�
�A�������+��� ^N@��TpC���y� �@���N�
+�@�?����D��U�&���.t_����[�2�Tj"��(!��8�y��S�H����C����"���@fP��$⣒�ͰU1+eu��ic���h�I�	���(f�ubW�B��J���
+r�R6J�Iƈ#]}8Q�?�A��'q�)NS�S��p�?�!�Br��lp"�ă8�J��7q"�Ii�_|�~aW�����ҏ
+�I?)((��И�cr�k��R�dpx��l=��4,�y�"�PH�桏Q�E����&z�)��U�B��e��L}B��X1�qǤ8̃sQ�&&#���p���7J-)Y�]*	��L8$8@j׏�.B��z�1`5��p.,<�� ��{e��v��5�lCL][���A0����%+���,�Ɩ4�#�G"������@'-āNdL�����:9�4q�Mo��v�C��h5��'�����k5������ߑ!�_VU�� ;�\ 36�0�_hA�����R���0�Rq����G	��
+�o���Bn��=T|B&	w���Ƶ�}�Q����Ҥ��t�-IXY�4m��td=�(!)!�l4�y$�n�+X�q�J""��6'Al}Y�6��6^�
T��,�����޳���fa��M({��S�4]dY�,���IOh�
+��e��fꔸ�J%�QF&���P�4[`Y����H��fX�>9��Yv}���(�y��I���]��-���H�I�>i��d�G7��nA�L,r)z���SE��E$I�����
+�c�0f!����=���@�!��!�޸�cwc��������]�����=��e��� _	�7����5��$���'���W��l�?��U���	�����5�h�R �k�#Q�U�&��V��T�پ@�S����y�
+$~a�-����ȊT�4��]��r���
+��*���ֻK�V4���PbN��(�����-�D�-��-����X�[�h����&v��G/�?P/�C:5�8nZ"!��ѥ�{�2�x�\������}�`�L��-���(�����JTXz��Z&)ax�Ƥ��~Nx�N.�)��8�-�����R'2ɣ�G$��(��=�Z��-�Eb�#,��G��/sg
++o��F&AA[�>/�b��f���\$�ibJ���	6;�1��r��l,��9�Ļ9Y��X��Q�#&b�#���`�"��!�o�[n���$�����w�b��A��2̺)�I��
Bc�ĆX��
KB���GRT")Q9���ػ�s��I��ܙŠۗ��.)������Dsӻ����E�4�&��-�p)�Ա%��ǖ2PR�DC��>i����C�ڬ&�Y�������R�F�I���F
�(���Q�U�a�=�q�M�ak[Um
\Le
^T�vO||� T�8)Rv����e�}�Ņ����\��l�!�b~��〭��C��"��_"���#���!-,�И�[�c򏉎I?'=&�����sƒ�o�� ��$���Q�2M`a�L��M��,j�?�ܘ\I;M��7���14RI��G�2I$]�D!��8%;�\���@��+Qj�-e�C�l� ��x�'�`O.�r��\�Q�x��-��VO+������]���X�gV`���E�U�E�M��;���3�Y,Z�/_G2MR'u�����_VTKBV��C�P�)B	��U�/�I?%C(��D�#��	����KP��EP-�@ZF6�lLr�)~XJڪ��b�ۥ*i�(��1�pRvW��*a�ATQ����nPh�vT��sW�ԂZ�Ԙ6��a�r�'ȗ`����0c!��>��pDv+��}�!X8�ЌI�ܡ&�ȁ�B�=�r�)�\�����,�������9�a�%�q�9�q�9�q�y)���2�k��-_h9��l҇�h��Q�#&c��K�OK����JT�]$���N*3�$�N:���t1˛„N�}\r��E���$������ɞ%��C%U�#��mgX�<iTi�.��/3dB��="eA,���5�"�s��ޅ�F��ܰ;�:cɜ-��^SBo-T29ʈ/K!��(+���+�j�Ū)G�!��X����eRZ�a	��5��X[�d+cp1�!�S�	��,i�R��S�R�)�$pk����)
+%�3R	�SQ�#'؈�fa��M�(roh�~�u�](�
+�-/�b���u@�1A�$Sƕ�L[ҼL��m
+(��I1R�IR�Yhh�#Kv���� �#�+�%�,g�M�؅N��J$[�:ePi�,��#)�Z��TH,�TB"�
@�ݬȸ��(��)-%#\O������b����I�y�J�#�.v�.���)n�-��E�z�3E��a���^v����Q�D$R�)f"(~�M��7�/d�*��a�B��lˆi(�S�ȡ�ꔸ�'T����r�k����2�`R�'$ǖ�I�?�d��PNt6&3�b�&d�/����>�=1E�44&-��]�iN��&0
k���,ט��?���H|&+i�4��5�Xb&�1M����/<j,DŚ
+�j
��?V1���{yY�/T��P�?hϦ��X� �l\��h�Z�Ԓ:��a�J��}r�ɹd��')�:OS�2mlI����̛J��"��q�A�)a��F�؋I�|�\�8Y\i�!E�3E�7�X���n�I�|A#F�5)7z���zy!�X ��F%�w�"Þcbæs��E��;򶊌nO�Q�!IA���Ŗ�I�[�H�m۽"7�֜�ö���1c-��������ö���Yn�wU�А�ؚ/��M"9�^t��P�vK��v-�C�2ɚ`H6����I[
+��˗D�e+I���ɔ����<6���J�
+,%��(�Y	XL�Ȣ��vGx�vCv�rFl�oMl�gWr�kVn��':�%>~ݒ�!q�2B$C�#?n<)A��^
+�
$J��!��eF=aA�i��ߢ��r��(�{b����Tΰ@�V�ˍ��%G]�EH�ge	��J�~����R�ɢ
+[d+�;�/3�	+m��D*9�b�B��Jy�d�b��x����3:9�/b	@g�r���~r��E`��@Y,�.�YJ8�K��6V1��Ki��BJo�R:w�rJ�EA�cTT�o=q�)�a�Q�!�������G��fr���/�_��Y�-�Z.�fi/2���/N�0�e�#�氤��c��H�����k@�l�$��!�?���[񸜳�l�^@�C��Qf�@���p��YmK��J�z'��X	�nE��])��a���I�ֺ��PVx-��ʊ�o�1�V`�5����|g�H�W�HM�EH�g�H����@��bc�����TZ��=�)d/���ͦ��fOX��>����t��#2c��F��0)�^�T�K�jݝ��f�.l�ʋN[YQ�)-��L�
+SB�_Ti���Y�jA"G��*�"딻�����O���L��x�n�������ٺ����Q�M��Ѩ�;�v��
�o7���v�Ak�!� @
+�������������`Ŵ��bڻ�������$%�Ԏ�0�&9݊��~�(��+�i���J�9�[L�6%.��+#�)�ڮR�m��]f~:n��qaV�������9��l�~ˢß mPc6<����1�ML�JYd+��*Y>$/�f�)o�Z�"�|��
+p��p���2�%�DV���?.�b�@�n܊��=�.@[A�R�V�N-W�Y
YF�UH���d�'H���n<�+���O+��F�B�a�b0v�,�,K���B��&��,E��TL��--<Y��H[�;ȺX��8��2zhc1rgX
S@{Y�g�tv]F�����
�1?x!�t\�lP�V:*;�������:��z{$�=dF�e�GXm=)����\��Z6tt2�ZhC��E�Z���E?����6q��))��r�]q�۴��5��}��F�v�Q��ba���r,%�\V�
_Dd3hA�/T!���t�ZAgYH��,�4��љ���)����n����0*�����K���p��@et��I?�!YWh*c�R*sa)�),�r�RZ�y���(KS�ⶡ��?
+�������c^R�c�����B�B�����)*:�
+��ኩvB�ϙ���y�2�M��٘��gTht�E�SZ��.%�0*$�
QLi	B8gJ@g������+G��B��E�<���?Ռ��a
+蟙���A9�1H�̐�̠�'�7������]H:o��-ZdrKfl�԰�.4��E����'Tb�.d-�ū���q�����	�R'V���B���d� D����l�=�[����4�]�bUL8��0��V�T/m�WK�@���X���~1&�ۭ!�t�_kf���e_*%�4j]6�A�L<�OBi��T����$����Z�diGV�]�JM��;�v�bm�(��T*�]4�wK8�Yk���^
+�������n��I��_
+Eb�%%U�A*-+q�ONx�$.n8%4�~�}U��M��ɹj��\_a�˞����oE�(:���w���RL�Z
+��
+
[�J��W��'�B�氀�,AJ���Ҭ\EE흰�X(&j/-�t�*�3*�s��YK�&-ሧL!��,!��L!	�\�I���(
�	�kU�ZQ���s�*�2�L��J:��aO�B�W͐�	R(0�hk�������
JF;(���,�t��B+�� Fd�*�9g�j�ZG�]VL�PH)�+(�+(�3L	�����_ZTkUR{��,��B�Y+��F䷰�����s��1Y{n�"��M��c�cYG��"�g�����du��h@�#@���h��B���R�����@�m("� ��C�ڶ��{�öc��fk�ag���]^P�
��_�ҽ����3�8f���v`D�K	��#��b�Iė6�(�d�Ϳ>0�-k%�݉D���A���ZH��]�y��<�Tʵ
��~p����@�x���O�P���<�w�Qo��9D���0e�>0�}�B�}t���
+��	��G�8�WC(kYF�WHg��3so��]٬��Pi	E<g1'#�)�^P�+m�P�wJ�~�U�ߠ�cm%ĒN���G�P���h��|�X>2i���|����`QP��N�ʹ��$��cs��T�UetV��~BE�/<1�z������ZM�K��$}T�bW�Bk���Ee�²��Ic�؜#�����l�lP��p�TD����h���9����I[ ":'��$j��Ptc2���G�"B�aIO�9s-��v`�V8���v��í�t�
+�f�6���M����\T���*�xX�.����u��D�_�B�{vm%N��`�$M��|��,m#�sV1M5��(s�'y���*����~�I؉"?�oݏ�+�^�l���\�Xgɤ!蘬�D�]�KW���4l2�Lç6�Бeg�Դ2thodZ������x�:�_�l��s�pJ�	Vm��]����(�(�T}����%�
Ԙ��>��D��c足���緔|j?dQ��x\�K��ﵳ��z�-U"�3x��8�d^-d�����[@<�	GB�50��U3$�ԉ��d���跈x�[E8�/�S)�7���,���(��(�4V�L�i4��"�';����lU��Wو�S'{�f�*���pDT��$t��9?x1�Pm"K?�H,(4���Z@�OL}#�4�➝CHWA�y���B+��UE�Π�tn�l���W���V2&�0��������E��h?x!��`j��A��I������W���'��"������$�A������D���^���ݠE�ri+�hU
+~ �=�T�.����>���4��xD`"�Q�'q�����㟝8w��)�z'k� ��)��k���+����b�ʰ;�v;�y��-[�6+�����9�8�V���<�u=ɓ��p�p��(3�.���ĀA�}nX�W;,{��
��$j��dP�Q��;`����X�\P��,�sԉ�z���/iz�$LB$�	;:k,}P䞿��y�:�*b�
L��� �GPb�O�X|*-��&=S�׶�aٵzh�Q%��%_�Y���.���t֐N�R�Je�T�-�
\H�T.&�)=�_I�k��x�
+P8J�0��@��P�B���4�����t�|�<�����.P1��ONx/$����J+��֎ˮ`��$��C�}7eߝ�9x�Xc�+H��8	�N������A�}~�ĢO`b�3�)l(=��@
�������5�k��ٜ��Gb�'{��
+|���8r�Ўh2�U�(��
+�f����2P�������=�>S�F�̙�2rf]�Ό�l�:�8e�2��v̀��zR�-<���}>F�L;�����!԰��x�C���Х߼T��
��J�\�`Dc��Ŵ��I5�}��O
+�T4�Ă=(r�+y�$��u���jh'y�J,�#=�C�����$������\LB�T�:H�W;�~=+��X�����aG%
�z������~G>�B<k)�E�ϯ�R5��H0���t���@*�*F�b!�2���'�+@���x�Q(k%�n��'��f�կ�©���9;h�>�V80�OB����Ɲ0]=D��*�c_Gs��Tß ����a�/8	�����]SH�70i+��a)�k�ֹ�+��]������뱣Lmȋ��T�4�O�P�\Tv)T�O�h�x�ʊ$�ȞF�ۣJ��'��N�$�\GBkM<�X94iƿ����8�Aǣ�A��P�U�R��?��5�����7�m�f�����l"Ⱦ��ß@�����Ă=�x��P�����_2��\0�,=�:�9�H?�T	��w���ۯу�i���p�6gpΖ�3�a�>���0b���j�����Z:<�Ϯ�#��]���w�xu�_Me#�f@���"���M�C�٬��߰$�K�@,���{�A��M�__�c���d�c(���Z@(
��O����X����v]!��~d�K��ZI��+��Xo)�����P$�
+��+,�ZE���v�#�]kf]c�C��M���$�����!�?�4�k����``���R����lK�&�S��A�]AD����ZH�G�D��K�M��z�D$-a���kʩ<;!as�2گ�h�`T�D��^�[�y�<W�����#��Qf�o2�z�b�C+j���謥T�Jr������������X1��W��U�6
I
BɈ:���s�ׇ�n�F�A;?ɷ�*=��H��ĀwO8�B�߅���2���nPz�D�ü��������0��Ф�R��D�A�;��~�o�4m&��6�Ϟ2�hWŰ��vh�jX�S&{Rg������]�^��O��'��I��N
�T,��@��a��1>{�]�<��K�X�W9ĵ��?h����[��n�LY=&S���1��y�d�I=���^0��PJ�fIT�_STkJB�V
L/��'����k��虾4-& 
!G%��g���١P�wP#ߎ��h���XW՘��hd@��j�X?�A9Oh�)K�0�J���I�!쨤) 	�/$	�1�5��v
F>=��t*��ӥ�4yk�͟��!�[p�Ѷ�A����N��J��7�	H;jT��9ij�8	m
+O:i
+LBe���
�M��k����;_;���1<9�;tQ�ä�~/,����B
��b���p��h�nX�	D0�B�~�N���)ģ�h�^LTkIHg,�}��P�-�z_`唑3��$��W��B�~ CS�`Ed7�zh+m~=���m��j=��IԻ��Q�ld~���W�|�:�i�����H���$�-wV�[�ƴm�>�.����8�w������2td���g�k]���gC�A�LH�K���M��]S�f����3ui�z��B�Ϡ�d��J*��Ji��$Tv"5�I�]o$z�[���W;*�����M������A�|?
+ц��~�4vl5����k��}����B�P��4��z���w���ú:\�ll�7���̛�(��C9Y锰_	��
��n]���=�t�Mߚm�֕2�����3�s�tu���󝣇(�>����������ߛ�a'�3+�t��?0��KH�)sy�����
TDk'��K����c��}�Gf���IC�~!I��۟c��h�5O��V���Ș6�������g9
+�L��-���m@��~�4��V�(��$�%�n}��`��Kf��"qr��%��֐Mڪ�e���S�"�؅"�|S*��:�)s�bJ�Q9��NPoFH?�h�M����}D�nቨ���W���Q�����v�EB��I?x1�B�_�1�K�@��n`�fTv#J��;
+86�U��=�9G���p����o�W�ԭ�̡�1of3�^Zm���q��>��MDٷ�l�|��i/6�k^j�g���2sh3�t���a�j��}�(x������Z
QL�2(k���	���<�,}q�ZM�J�����C�&����6��	|�A�|D��v
sVge�.{�7����ji۲J���@	p��k��A6�Gro
+c�n�	���,��4�u�)��죇(�4�u�d_]�XQ�F���a���6����l��Z+ �J�d�`�弤�9�u�'��+������
RT�@�|�.m�ዣQ
�O➿�m�0f��lƌ�r{�����[Qeq0.� �>��٣O���Qk�r���Tk�˨d�'�T�e!(�Ă0@
+��%Ƈ/&�
+TL�F���@jb���j6����Ɵ{���\b�(��ڢ�Ŷ���JH|�'�����Ǿ!�)�E�ƠD����T6��8i�O�a��F%=���6@������״�o���l��l�J�-��}AG�@Fc���g�Үp[��'�n�����;5����9Ʊ >p6�-��2~ecH��9k�(6��5��ڱ��;6-�	�:C��^R�UA��]V�H�*�j}*�E��Y.����Q�A�M��X�&��M����hjǴ��ۘ��tFllo'-|�7��0*�~kH����-'��&�����l#'�t�f�E����f*�����\WN�XK2��uR(am�m�a��0^����+�i��p�|��ث�=��'���a�mtX����H7P�(kH�T˒�ӒB�`�4ubq~qHSͨ�Ը��h��a)+hI#m9��:`����.��И�9 B��Rb��
+�]��EbB�IܥRm)SF/����J8���.����V���K��.�v��G����=5"�j̳�i[�g�Bp:�W���U�E#��呬�e���`Ҵ^W;/63�&�D�0+���X֖Q1��ک��0�M�2Ïm9A[��8m��&�>
+�⼕�s�J U��e@���mGi�o&.�v		_I�����Rk1'��1q�����T�o��T
+�\PH�
��+G�UM�aʰ�m�g���M_�����/]��xWU�ic��ĸ�������x��!�>�6p0��
8���,n@�� 7���ɗ���	1	YH����Q+e)�2V�Mm�/�2��T-��� ����o��E4;���>,���f�]
+�������32�."�
jXj)��jö���YV�`XN�\WN�]YT�*%�/+$�PD�	D@�- �4���bqN��:�;��'#H�XyY�����T; i�T0͔�*���\=�F��p�fɼ�tp����m�n򵭇�08͡�n��e�T��(�YK�YMʎ��
+:�Ņ�����b��b͔�L�)�4וT��-��&��&�2�ʹ�r��a��q�os�(��Y���Y�#�Y%R/݀$=���ւQ:‹˞TY�}�r�p_n[s���Е�"�*��
^(�M�_ہ
+���%��m�<�W�a��v����@���0K��GäaCp��,
+|���vi������(����]�9���*6�H9
+H��
��i�N���R"�
�(�M��t�.�tk���Hܾ3~���qk`RJI��>z��Ǻ�SZ��,У߬�y(�]IA�NP�bNV}��~�'�e���͟��	����l>:O×6_�̰9�:��1O�Z_\�>a��ʀAFo���S�e���h�߲4�nX^��������g�v���6��吀b�4!����G�7&3���K�^:ƮLƬ��1��3��w�1s(�	l������^���|c)��b�!�ua���eĘV�,/��*�Z!1���7I��J�^:j��|uCs��%U�zr*O0�IK8�Ys�BB;������H�܎�I�1@�%4᜕H�t���;̋�a�h�۲E����r�'��Ө'K�X��<�=�,�b_������b9�tڡN��R�0���Ƭ�D�mX@�X�R�u�E�X��V�P"� ɿy���v��R溒��:�t����Ϧ��UA��5����1'g]R�U>�2h��*�����F3���$�3u~m�A;��{�w7����F��Ð�y&N��$��9_�:�aʴaW���a_!8	��	8��ÇxP�v��g����Iqݛ�b�/�9�õo�!�/ʰ&�bC��X�ͮ,498#H�8�ȒB�@	-)�Yo%���TNPC�fP�c
���r��Įs���`�h^MP�S��$
+�P4�M���(�K;�"�����HNlg'$lQLkHH�XGgFH}�����V���Y��z���azi�+�Zƽ�oI��(��\��o��L_�v&
{����f<f;�[�����]�h/����7.�`]c`acb_&W��+n�H��[-5�&�X%S��c�G�0�i3,0"'E�����Z^�X�^0 5�A�X,/d:%=~����.#�D4�M���Q'!m�)H'���ʿ������?pQ��OLxJ��_Ɋ����A�Jy�ĢZ�y�uv��`����΂�X*�,���F͋�L�i�<�A��b�l��]\�[
+��avÎN
�&����?�����0n��U0,/�aX9�.�`Ę���b"*v ŢVI�;#y�H(�K�ߺ��ޒ�΃���`�S��#]6(�O�
+"�b0�T�t��2	�X��UT��#)�@LY�K�`[����E$K�6�-,�	�3�A�~>�e9X������J���
+�������5dU�����r�w��[��pa�
T���'>�7�2[;S��GB��1B��0*�
+dH���b9�6o	�/�JY�ҚPm�)��W�~Ƥ�}?q�وиÞ�����`NF�ULe- i��?�G1o��([���^h5�2h!Y;����'����&�5Uȿ3H��~�rƷY1���Ck�,�D��2��	���	[��-
�++����}y J�����`�(���`��,>��q�i��G���G� 
+�
�EK��;�!6U���R�6@�ނ"��vAA�m�Ĭ�ȑ;-����$�5�Qk_�1�rW��}5dS{�饏8����!]!J�<a��v(ӯ�)���	o)�E���d�8'��I��uOc�,�w�����2tdt
�۬ӽ�i��f�ų�2�&[�������:40�.]�� ��6��8�������&^!�_:���x���t�0���j<H��*��
cb�.İ���%�A��2���E��	z����e }#8'��UF2� @b�2�Sa�&��$��>̊	���Fy�Qk�*�"d,rY�����=��l���ڝq�ր�{i1hv����#(��Di�;�5%�Q����Ͱb�Å
?�yڮ���
+��I��Ig�6�?��#��ָ�yl���x������d�,��>��-i%��̐#�vf�D�\(1�H�u�D�MSqk0�UK5cr�����`�tF`‘���C��]kf����yA)�T$�R0�S. �+�MY�Ls��Ϻ���aިү�j!\�y�0�uŁwa#wgu�h�;��hxV0�R�c����k��q�I���خ��̅$0�c-^ƭ]><�\<���!�L6A�ig����Y8���a(앓�&d)(G�P������%�qw�!s�Q�k���-Ɍ����y)�[?u*z*�}
+�Z�T�*��6��j�gŽ�l�lg!}5�j�f��Ű5��(t�<�4��J$����_�ܛ�F�6 ����Y-!
�:a)�4�8��*9Ft�ù��d�AJ�hW�1��::X���`��C��H.�3+n�e�_X��g+)����ڵ3��z=��	���4,*�#��Ll�����D�]eaXTg�ܴ3{hYB4�Q)�,u�1�e�chZj�6%.�aOP�R��2P�V����K�%�^P���FvЈ�m��+cV4.k$���!B�5��3����'%A�HN8
+~8��,`u�k�"L+�R�X4�.�Y��ٓ2I�3R	�����T=��v����P#�kF����j�VL)���\c#��W���4�վ���"����a���]돯�I�[K����lP����.+�~*E���C�o�@p�Ә7W���v+3�7&8���M�ge���P�IPx����ű ��p��dz)�/�N �ٵL�;Ƴ<�2f�@H8��ec%b~�D{�Z#�ml�=��a�xņux��AO�C�g�͎��`�@�2��@���ՐMyÕT+E�����sKjl
���
鱥���Q`��R��6S�a7��j��hj�\T����thj�H
;c��N�	�!f�ډ%���y��������P����](�7��{�ˏ��YM3�%t8J���"˚�
/f�M��S0V)�v;���q
�5���`b�q�jqm�H�'���cu���BPй���7��H���E4˧�ǖ�I�-�#�	N:�%Qo�	���me����#�i���B;�e�\S�1{(�1��b�����q�a���c���T/?6��b���bKZ�f��hj�.�'��c3yu`�j�w����
+������Q`.T �n�E��� wq7F`l;6���Ȇ�\0�}�k0�F2@���U�
�5���1�U��8�5XI�����Eu��U��GG�b�˵��^�Ie=x�偬�~(���F���������Ql0ʓn�����q�&�n�^�L]X�%cXؘ�.Z�/u�/�NU�����8@������PT��C��
ac&^�G40!��"�Y2&��_�K��"B�O<@|�Ƈ+��p �3O��Ǭa�D�܎7Z��V���u���z��nvE�w���L�����)QAc���Ԙ���hf��V�%�X��bXn<�76 ��…V#���U�Ѱ�a�S�b�5���HX���X�؋m�D,�XyLL/�;�R�V7���x�36L�i\�I�
+Jf�����=\�����$.�A���WQj`����@�L��"�*}�E��P��J���
+G%��	��%D���c뼋�׊�u�#�0~"�J2����h��X�f��0c"��/:d�op��,^�W�o|���I��B#��]""ĖK����5��aL�K¸5�=^�Km\�ic���x��P4��C�Qk�\O�:��R<����p�^�'$�����pu����eF=7aWՠ�g��jB��P�'y�(��q����b�����i@XژH�k�
�Z�K�/����XB��P����/��#�!l����1��Z
��KB�-�0hD��w���X;.��~�Tn�|�M����Y\�>!�c%%q�� �d#đj����W,'O_@5dB	K���DK���
+pD{��`��,���1o`Aˤ�%훤J\��yq�5���!abc�Vc_��z1@��>��X��~
+(�E���,Ae��@��xK��1/
c$ �4����yZ0�2�X8��)�����p�
+T\�n�m��8H��l��:1q@D��H�#��8���?��R�E�a�%������f��f��c	ѹP�3���M�~^�!^��*��bS�� ��8z��,_�	��/�����a\��8�%�hrEY����03Bn�rk����T7�&��OX��p5>�55�qai�J�G	dV��|�;��2~(Sq���T�j"����V�w�St�3����N�ô	��}+��Ȓ�y���Iy�{	��*����y��R3	_��K֏[A���q���%�Eh ������
+�%�cd�1�P����N��+(�W8D�fl:������G8F4>ȸd�)��y��uh���T?J<���� �#�� !]
��	Ȝ\!J@�F�\
+i��jI��L/�⮏��3,�=`d.�F�;��E��bv�#O7>@�n|��xɆc\?E+�����!����"��E,�����c\p0��55�q�i<����
+25�"p����o�#ĝf�x�/�	�J2C|iF����q�;U󘋇�p���?#\$��\����s�unS1�f"��M�'r�,��e�2��4�͐��7��6��a\EÅq.�1-+�bh�Y�M��?�Q�;.)�<q��C�ĵ��x��$N���%q*�%^����&��hYQ܏��V�7(%q=D�)�#�����;EA?1X� 7�2V(�J?[���a�b�k\@���Y��22�<~��("��YY������@(h��X:@�ȀT�R
+L�p�LV�)�pu�怋j���k��A0�iK<̀�P��- %q
+GJ��c%�,�#�eD�/4q
MG������x�$t
�<���h9G� �(�IhĊh�HPA&�2�:��'!(��B2�;phzy��D��Y��cQq��Z�� �56�al���xUŽɫ�p��X.'���=(���8R��J�`
+�x�����8��:����q���k�@<����3s��
��`�<�s0	��@(]:�0��������]C0�:�`~q�k��|�1I��ΰ�[n�E�؉�r�f]O�5��^O��w+@�'�F���8q�H<�W�#ğt��SN?�cğn�8�U��!�Ûj���"~���;�q#� ��E�Q`@�a�{��Z
+iR)��c����"�H�wG
L���9����S�
JK��� x�X|۸KX��=R���d�%v�v1q��%�w��{5D�/(�gA��E8�eM�Q�
+:�r��">uSĕ�B�G��p.�y8�5�?��!~E�9�0��eٙ$/��C�树)��E�h�N�$Š���+��e����!��=++{�%Cy���BW�Y/	@��uZ��Ξ�7�Eq�O�k��A@�`PG�K�cqCE���?Pč�=���Û�@�)F�/��kG��GW ��Q�K4P.I���l2E��d
+)�*�\%[:��S.Y�\v{4���H��Q�ec���L����a��d�����%r+FE�_�J����%^6�d!H�S*�����Po�C��q*�"�@�G�$�#"�Á���N��}8��ױ����^�����!J>�x�T�EsЈM�*b�96q��,����	g��!��4<G��!�x��6��a\�kg	���:���Ļ��xX�����l �I���_�2���A�`����)�#nt�=826�z���"�F�m���!��q�����c�%�!�n���;P���f����hb��{yĸT�y��#��F%�F�����(���W^#P���xWR�:��G\���J������
+J�T9H��L��>76���p.
+��S4�
�`nr���������DŦ�G��. ��!E<�=�lzy����‰Hc�%�Mn�,m�@��,����
+&�C�]��z���8!��&�3�	�S2C�i�']}8��><���^$�Ýf�vx��71=X�"6�Al|rYY=�@�*�R�=���KD�iȖ�/*��;���{H�L��yiXC梎��S$r�'������M�El�Q%��i�8��Cīp��� N����;B�
+��g
q��#.A�G�1�Q6E?|g��ps�1���*��������g9���e_+�`�`������<J�����3.
y��M[� mx!sd5�Μ,�L�y��cS�ȣ��cKw0��5��M�^P�=q7?�@����Q3A���Ó8�;���/�q#���s8
��1\&�_cV/�X���#��B!���F
+/-�^Z!����p���Q�h7�h�"��>6s��4�qs�h�	'jkcB,�Q+�|�8�\���[q$��{l8
���1|&hg�����/����_20�>fdz}��$$�ij+�5��G���G��&�^B<u(�uB��C�Yz�nrE5�qm�q	3'�$p�-��M���9|��p��q8��84Wï������>.{��K܂��w�����&��H�Xz��؜,���y�0�q�������!���E���N�0U��]�F��di��!�F3�+��M8\IͰ8܄)��(�C��p#�Ãx��62\&����Ns�13������
��:��jı�\׸�Nva�qp���f�첖Qq�5���X2s�5Rs��W�D��Gf���62�@Xpv��,�!ӻcFf8
+L����C��0{c��A���8Pq0xT
���ʯ<F���50	�B>���ƍ����kc/`�A�-�X��qr��C��1�������fG����S�����<J1
y�f������osSI��(n�DA�<Be{��Jo{S0.�5$�����K�q��e���0W�Áx�7|'��p�a�
��C:ND�J907ȍfمR�W	g�.�t�(�S�.
�l��@RXr{�}���$b8)C��&��0%���Bn\Q��@����q֡�9"
+�#.��x	�;�"�`��n�iWT����;��J�yx
����;���pNs�sί<�s�9�s�e8
��9����^ï<�����}8G�SGS�������nk�$��~PphfF���y�	~�
+�z���JAV��,�� (��),�8bXzw�{wԸ,ԡ�2���ҐG)f"��KD ���;2ŋ;ė�l�m��?��c����!<�	v����2|��pN�k8��:����.�e�����o�LYNc8;s8;4�N[V�,ZNW��<��ʲ��eU��Teu�ნw�E���ה��dVdȦ�$8m� �Q3pJ�g��u�3��@��,m��C��c�g�!m�+*j�����KP��_ٸ��J ���4|l4���W����j��Bk��*V�1��Ա�5!��8�B�}u������E��݈��8E�
R82}vo�H@;����Y��"�>�"]���?�{3	�,"��n	�&m!���Ө�@�-%B�C���>z�Qm&�?B
Lz	����ǤYh=�}�H�% =���NZ+�f� �%]U�%��%�±[P���:�u?�zf\�U0&_H����oDI��2�U0���!ގ�.��Pj5�]z"˿�
+�Z0��S�/�gwr����^\���y�Q0Qf�z�#��I��~��Ў*]4�����W1,���6����1��L��*�F/�����;_<!Ǿ?Ub�;���0ݗq;����د���5:�s�^`��HS��|��g�7�� G�σ�W���}&P�w
+��>a�[�+�����_�Ա7���<	�����8�oހ��ʆe�2M�7~r7�q5S�a�ґ)c�ؔ%���JH�N��=)���>��s�AK:���d�4}m2�[�D�{�zq���r!N�(���$j�&	}��I�O��vSh��8�y�59������P���AKZ�ǧ���e��	>���%q�D(��No�d��YKȧ��Q#��";�6�-�m�ج����,U����� E�"/H��)��' �-���W:*������9�1`�蹖���|U
+��Q�_�>`jhA�1}�
PLz,�4	� ��Qf�?Y
+�,�U�
+|����0R��I̋��8�ru��Z=T���@��X�Q�����Ǫa����}�C;�»_��V���$OŸ�D㧂�G'ev����?c��2ok��
�^#�g��F��d��^Y��w�u=hr���S��&}P#ߟ�C�1jfx`z��?�E�㍓�@�{v�e׎ZM�0C\K
+Z堬�&%l�>����@)c=��p�k
+E<�72g-��(�u*�t���F���I�Ǯj�Ϋ���*�X 5�BEl��h�N��S$i���������K�_�@Edw�R���9K�&:}�o������?�����
+J4��0�+�����9Œ��@i�N��~�I@?U±'8�x�4lE"PM�-���|�Z�x�&٬Ú�����
6��%ߟ�;�o�����S�x�^JH�"�4g�?���(%�ɬ�h��Ro�;��S��^����a[�ď�I�}�2	���IO�u�3X��2���3l3P����8�F��ό����B���2�#av�������)x&����[�o��j�B�z�W;�z��̙J��T����>@�������lM�[�$Z��zd�W6,��(�����|���'�j�n�6��}�j�F��T�����L\}}�f�*����ށ�/e���p�z��G;�4�v�"�S���K���'�����������cFP�k>N���p����Y6�	����z�hkᜱrd�XA�*&�������X��!^��іR=��L}��=��c�[;<��u����Pd�}���F����u�����k?��{�f���E�M�_O��W���ii�ִL�޾��SK���\�b�+m~#ذ�u�rH��H���5�=V+3G�3��FR�v��)�5�}�)���,��'I4�N���H<�$��\8�+�>�Ff5j��{��H�W<(�T�Do���V�,��;.{���t�'a~�R��`Ed��$�F���I�}A5OӗV�|���'�T*�����t�<!]�Øw`�ж�AYC�AY@5�G��}	��Pm��_i�����9�9#K��ȕi�M��Z;ƻ��U��2w�L4	(Vd�LÐ�.��w����>�x6���.��i�̺��v�f�0.$�]Lo�g��1����S��Z*�Pr�Þ��s�X�{A	H?#�Fi��T/������4��h�Z@�Z;<�
+��s�Q��
+ڧV�8�۟üm�"�u���pd�+�vT���$�D����jF&��c��ځY'0���ɽy
+��P�Z96�+�~j��g�(RD;J���/A
�DZNV�v��݉԰'(YK"�� �TK;qs�2ڱ�h�UT�E��g���	��s�qu"�o
+
_�j�����3��v$��K	���Ii����O�9c ��?S���ʺ�	IoDI�}�{�������[<.�����P��ر�4yk����M��N�����R�)�	FO	�	�!Ġ�V2(s�@�L�\��G�`�w����<$c�,��8�wP#�7����L$�bX�Z?:�,�4UI�@*b���?�{tQf`m`5ܝ@
y�uL���η�����2zd򵭋��e�b�ny���� ���?4o3�Va
+�XBj/@ُ*o��Do$x�إ�<�r7
 Z
s��5�/�ǿ
+���i�ju�&�v=�-$��&'�)�t��~`t�;}~��8��<�H�wMz���M��:k�fW�$�i��h�3�F����)@�x?h�hG�*�%���h��;uZ���e��oߺ�m���u��$�g�b�*��/ !ٝ8�[��A;I�k70�hG�&r��t�6�n�L��pm���u(UD��ɩ����<e�Y1���W?*{(�4� �])�p�����C���Ȯh�=��S;�΀Ţmቨ<A���Feb��N������X86g@>�.#���ڪ���C����p	l?���\	?8�.�ȠD�dF@���2rd3ی�1�f}�l������F�/{���x��s���P4���Q&��:���p�L��#JB{J��>��k��j�y�k?|q-����/+q~+�Q栗�K��ov��oM>��>�z��]ڼ�����l��XL�V�T�a�f,�@Ġ��6zr6�m>&n;��m��2�G��Ox���"����r>��6~ou��[NW
+�RpAG������Y}m�Z��Y�y+�k��<�ޚ�f�N��n��n��A��i؞"���`�K�]�>�#g�׶Z�q#�k���"G��	�p5zx5��:
�)U�L�K��~�����+f�P�z�,5�)Lg��vY1�Ţ���d�V[$��Je��(Ɏ,��r�p���p��U��*BڥP?c_W��v��>������2"J�T�ځY_�����v1$�vQ_#�;�v�D�}�)����[0���P�)ij� �>T颟��8�?�p��Х�4ui������r���|
+��F���o�s�Nq��4����;��!��+Y�y/0����k�-G�Q�?h!!s���U3(��=�b'�O�@�P
���%Qc�ö�9�A��fbc;�t��e]�c�6��3yf2��`|iX��.�6�U͇q/���0�9�9�0����:U�V46i3�u�j����|��T��%�s�rJK�HL�$��*o�G��`"����V1����PC�i�c�n�f/�E���j�мТ�m4�wIn�Āۣ�A�I��f���D�}7�#�]���e�5�^������n���yFo[��
+9� I�/�G&i����HBw�v�F/���C�{��%Rn���4���s�q6N��3X��Ŕ��\H>�ϡ޿���� ��T
+ĎaJ��DD��n�4T`��78��d�m"m�ɮ����1�5��ndv3*s�u�揮�9��K�^ɒ[���g�к�ߚ��S�7~n�M �m�v����뚗9��o�”@	����{�C��
+�Q���|���f��l�������
+T(~�H�I�{I��c��{�x2rh�,�������:_��D�ԉƮE���3viZ��9�L����0���k�ߴ׷�1�>6mS��+y�5}o_���v$�6Cl�	)O3/VP2�R�����x�j$1׆0��/�z{뾔��-��I�I�{�����\W���<����{1�y!�@$�@�Э�|�|�l)�[���)tpk#�f��'g��u�ܯ�c��&Ʒ|݃U���j�T�#Hg�ȵu�.���P�>�/�V�M�
+H�H$�`�����h�LA�B�Ӭw�‡%A�!��V��K?��h#M
+����nD:2!I>[H���컲�����7�q]��M�9�C�r��s�o��ܶ��3�g��z��v�״>s�0gZl�a\�:o(�D����w�u����Ӱk�Ն����xp�B�}_�+�i��<Ο�ΗΦ�kK���5���#�m��ʰcP��=�	��I(-T��ldX=5l'`}��f���ronB
�P&�x�Ø���<�Р��9\)M��%�O>�PF�RH��'��	跚a��x���u)�E���#LZ����ؚ@�9
+�� E�oP�y8��5�j���֑6�h�o
+�!H@�$��P
�N!���y�U��������
M:�=3z���3�v�:�6�����}�H@�Pv}�ŘN�]�"�vڭE��3|i2�*�=��L���i� e�	���i�V���F�\l�W]���¢�R�$ƶ��UY
+u�C�*~�E���;W6�G�
+��(5 �C,	�R :3}vm�C��c���*=T���wΖ�3�4|g?�ww����	��W�Z׵3yjZ�=�,�m��֫Jj=?9�JV$�?j
+�3H��{�y�Z�tK�,�N�a��юn��)�+�c�c����ܫ��b�� �����)'y�����xG/�~��^���?k<"i����/$�m�3�5��Xe����	T0�S)k7 i)�t�ʹ���9ʄ�O�
+r9cxR*g@"Zw%)����~0#�d���W30{�y���Y}u�2��B\������S�X���x�qP�i;�~���YF�L���-(�+"�����
c6���u�N��g�b����sە��f"�z�3�u�gq
+�j#KnW�"���'ev?��O#��ID��	���2�ZV�Qn�R�HO@�9kᜡH�7�[=S�6?��i��蘷3�Njg3}~}��5���B����J��r%���́�tƽ�>���u3��-1��J�~7#�M��z�#�R=��Z0�H��[�C;��ɵ1�n~/$�d�x�Y>*���_���p�~�>e߽�I�M�$��(�n&P��(�q���p]���je��&pm^
+��1ya�M9AG��)w�]������I�V>(kHD�J>�M���X���ԩ��A�;IS�|�Y �l5�'Ѯ�"�x�D���:�ޚ��ø�
��;���:�u���m�)HS鐜ɞ��Պ�؆I�5H��"�H����[�2*w`J�UY�_WJ���(� �T/"{���Ge�%�s�:��$�dbHRm.#�tUI��/�Ǵm�<�^���`ijk�2Zo�2Jhz[��I�X�8�a�
\H���j/	��4��f���l�H����"�u(�u�)�4�%��V��)հ�a��k�ksO�} 5��p$T��Iih����N�ྥ���2:S�p�>�y�������i�H�'a���d�`ţO��z'SpW��f"��.]�Ϋ}�z78��1�w�{����}�RZ�����H鯇��i`��B�}vܶ���bJRm�	�~��3K�b�> �ؗ:k���:4�)�{S��'���Q��~L�g��f�\
e��X��L��Q�Ôu-��l����@�|~	��u�n�.	�f�����go�o�c^G�"�vP�d��G������+�
+I������ø�`�w�?�����K������y�y�&�#av�����R�B�POmr�˲Ai;���9���X1����ÓP�����h6����_�@�o��cV�����;�u2P�ޜ�8�e��'0�F$v�]��!�S�Geri
+���[V�W�Ժ��Y����,�%h�X?��N��}�Dc�ҡIk`�����ZP~�����Y��RD�	;:�/$�{��,,	��5��o�걂\�
+\0��R/��
+�ZB4�JHg+��i܅��S(�[(J�`"z�h�;L��xv� �D�5��`�ڭ�z`�������P"k�5�*wg�,�z+���sn ����_�&�M����8�n5ԈþU�s�j-�ڲJ{mI�',�.X��<� F�/w��Aģ;\I��IT�-�3��A��[�����(�7�����Y��&$a_���_Zo�Ȭ�*홼5:�;����p|�xtv$�M����3�o�꺎 �]���e�����r[�i��W��V��,�ׅ �z�<���k;t���e�nXj5D)��6���c!x���Zwgy�h�K�.aJ���A�m�Q{',�"�Z���.(�$�}���>E��Yr����JU��+����K����
����j_�m���m��yܣ�J(�@�|sL��̙6�����X
���T�p5���3|iڜ�8��oM����cG�Z�s��Aiܓ:��i5qނ���E�怀����S�`Ec���H����RLJ��R��Zk�Ф'�~�U5(�+"}���q�!I�S�js0B�����F�S&�%P����A�}t�"��x�i�ڲ@�|��Ï���=�wF��]iܙB���j��$ʵ�&�|$�h��:�𭎩+����N�a�7欋�̝ey���A/T��g����4mΡ-T�G���1rdr��V�9��M�_��s����oHdl���O:)��o��|�a�w����p����s~5�D��O❭�Es���~��f�
+�4tn�
 �Ld�g78�h;h�u�u}��j��H���A˚J�$����=�w�Ox��p���}6��F�
r����u�8��Fo��p��I�:i��w�w�u�q�4�l�	P<�
YJ�ż�c�@���F����`�ę��X���LJ�ee����}�>����2��KJ�WA.g�ή�2���h�� ����|e��3yh��c���Y�
Z�c�3��V������ab���L_����ŵlq0��iA�x�d"���G1o�y���B�uP%�F��1+���:��EFM�?�R��B��ʱ�U���8,�����H	2?5��rp�U2Ž`C\;�����zQ�1�=,1��ZG����t�Uv/]��C�^�z�&���yԣiӴ0iv�΋����7�r�u�c�p[l���>��z"�AϓhW�؝�܋��:�WM�ڼ4Zx}+�i���Ҳrdb=�{��&am�)�k��j5{m�Ǯr?L(���2�}2
��?9W&Ǹ�iu�ws=L�	:�
Kzǰ���y-���&�Z������mT9h8�R"Js	��X�quO"^r��A������Q~%N��Ǿ�SְU�Z�)K�P��S�y׆qm�3|lZ�Ϳ�J��zN�\�SH7�p�>�z�����Iēe�δ2sgٟI>�T���@����'è�cA�{���
+G�����0�%$p*�M@������cS���=��A�'��ٶȊ{�L���B�~u��a����=��۠���+�f����C�9�G���;���a0kZ:a���5/�~�Z(p1.
6�oX8j+h!�f	��V�k�h��X?8�����IN�[H:�:'7�IS��L����V}��Sv#�pR�Մ�0˫�b�m�
+4y		���*�2�P8���E/���D
+n��V��~�;���Fp��^
+-�0nu޿:��ew���6��fcs�s4���;x1Y3y��������BGC�"�c���e�m�pZ�7m]��jֱ�Ƃ���F�
+PF�P�kܝ�w������L�������K��]�{�q�r��P�/�~�	QF����?x17xQ9+uz�TC�xNp±;�"�S$�(��	��!�i�]R��MT����4���'�W���4~mr���F����Y@6g5 �B���5�)�2W�Ry�HZ�ү���Y5?���n�q�ݬS8�&�h �=�'roF���D���P�_'�C���Q� ͔ZH'�ziD��&0�&��S&;��tMyK	�̵5��eļ��)����3�Ȓ�g���l@\l3@�� ��VK$�q��5�jR��9�8��d1��L���=-T�E-ҨW���)��Zg�"J�4�i��,|��u�J��*@������B����5�$��.�߉��gC��\EE��d�$�60	OFEF�rb��E��:bJw RڳKVo�$�~�笵c����IK����4�gvAH)����k���@�!И�
+V��_BOY���y�2���C���em�B����e_'���J�]���
�&vƼ���C��l�lr���R-��� I@9F�{m��1r�����P$�[I:e]Hh+@�"P�8a�i�4��QBB�K�L�!���#���*�zi'�p��F$��5ۣ��o)1jm�u)����UYsN�b�C�QIk�RJ[	���Rk���Qm��T9CT�CY
��6�19'�r{N�s��r�,L�<����h8�����yHbH�@`@��b�Ak��S�08Ă��l���AG�Pnj1�����@��	]ӶS�\]�w�M�S�rk�o�l�a�)�FtDY��A�lL��cT&Acvt�4�x�H�	ժ�I�G��͜�6�t��g�s@��p�j�̗��oC���g"J��$*�ք�p�x��ů��In*�p������GoU���#��o"�t�����Q��!MU!������qV����zY���V�G{�:7'9�a��l�'�3���+p��,�ߔ��ny�03�2{:_�)�O�H�ՙo��zm��B���0�X�R½#�^R�@�C
+�3��?���s��_|$j����M���e[��?l���c�u���q|�J�����+�[)���x�0:���l�%��m
aQ�Y=�i�������'�E����}�{F�'�;��{X�.wN�3#��{ؾ*�_��U�T���h�I���kƞ�v{J�Å�VZ`if�K��yxߑ;Ӡ�D�.ϳu�I�ϒ���aע�C��zֿ�gu}����g?+�xS;_�˩�Y�}m}��e��0��B�l�Q�";�eL��l��0���XyOf�7���T��A���o6W�h�ǝ�g�!��Pt�x���S><ӽ��^��E����#��n�~2@.d���pBм����߽ȾO;�;:�x,M�f|~VEL�z��aL���?@�΂�ɮM������������[�y+���7���Wco(���a��������o0�~V����Ga�s]��ZB6 �tZ�_�����k���,=T�`O}�r��A���o����e�OC�i&��5�'C��x��&�m8�;/�㨾}g�Ϛ)�mbn�k�����w��ٞ��;����v�u�Q�T����
+U��[�Y���or���ڽf�A�J�U ��5�kA��#���?4�?kĬ�����c�8���-:��b���<�ȅ.�)@�����e�����Sқ��u�}rYM��q��?̿q䫬�W�ͥ���5��-��u*=�,0o�$~��7�:�X�甹W�.��o��C
���|�ҿ��<���n���N�U�,Vݼ�@�X�lIz����u����Z��p��ē�;?�T������<?���h/��ޒ����8|�l>G[�M)_���<�����ðaVΙ۳\1����N��A��?ҕ="J���o�7Jh;Y���������Z��i�;+_��/������y�+:��ϛ��N������#}=m��o��X����O�tu�̃��RE���q��0yW�d�<w����{:1��\Ү=�]�<���wI�O��ٳ�~5�*}��P�����0��^����H���M����[L�	�]Aΐvfg�s|�����M����]�my/����[���qwԇ>]�4�����D����i�ϏLO O���,����ٞ6�>ej�����a�8x��垙K���
+I�Yx!=G}��Z��E�|ob��Iw�V�
�9m�j�����x�K���
�#5$^���I<��ef;���܁��Z/z�������~vʺI���\��Z	c���6]��x�؟�O�\���⮨�lD�Z���27�q�	�ƭ�����U�^�48�{���=:8��W_��oo�2_<�K;������|��<4b'���O��[y� ���DS�U���Ä�
+#ᐬx?!F	]��`s�/bM͓3Q�VfF�����{�9�~�`��/щٿ9���2��z��c���V�ͻvu�P���]�Q�f�(m �B��݊�&��il���!A����V*_=X��w���/������9F���Z�5�^��eӞ�k(�=�)Bv�*S���8/E��O6������LԥDe�ޞ��2/z&�-�x��5}<h�|+r���]ٮ_��5�q����f����t'��>a�k�T����G$�N���,�#�߭
+��:B�Z%�U��^
�}��s�g4����X�+���3{�Sk��[�q�m�c��`Y��Lt�1|��/8��@
+�P�X�[�g�.�Н�R�F��g9�x��>ϫ�p��M�yq
c�}�'���i\힡l$��@}���/f1�-~+���t�3���b�嵙���A��Vo��j^��J�o/�0ڈ�B�}M�C�@w�a����"
+������x����f��	k�jK��\�)�3P�$�h���9����Oyʟa�pћ�qHA�0��К�n?9���z��	{����7£{KSi�U]8ۀ��!�o�;"�c�3�lT|-�s�/��f|�>$��.�yG��"2G�o���)��&�~�p[Bp���l���W�W۫��g@!�̳/y�;ȅg�BϷ��V~�q\9h���#���C��'��Z�dx*�5�\>{Y�M?��\@�	(��a8J���mW���?*�>��;�=V7Jh�F�������4�Q�_"��a�$U>Pq�Ei�S��H<�hl�\M�T�����:�e��{>g`n"0K�*��pm�4_�������тُ?9�/گl9zQ�T
�;߈�mx6�U�J��������)cq���z(?7� �]��D��������=/��+�;ˇ��V�b��+��5�
+L`��.nGh��`�P
���]�`-��YZ	��I�f\-�9�V�#����Q^��Π��gM׍�b���r٠U�Eᵰ�&�h���΢���j|ґ�oxV�;�`QX<�~ۥ�?h�ΰ|��kിq"T�:�-���ˉ[b垿�F��(*���r�-�dp��B~(��U�'��I��7�*)Y	��.�]����&�]i�!�QK�}o����Ĝn�h�hlRYVLkhS�OgF����
+�y�R�KQ���>ϟ��`�a����X:�%�	�%��r�a'����i�iE��O�
�T�����܂3�ZI1��J�|V��4�{W��;Q5;��Ӟ��`��s3=�a�J��@����J�K�-����&M��<@/1���}ЄU�h�?�c��ο|�Ob�g�L��mt�	y�{�]�}��;��rf�+=w��d	p�;ӎUh�����k�D��=59�����b��
^�R.�-�����RZWd���ex.ɰ¸9��)��l�;�=z��^�/^CL�@
�C�Ż���޲>1M�e
��OS���0�U�N��յ[� H�Z�Bª;�QP��3��K��~��f<H\�p��*�R@��+@g>&��t@e�cq�z��8�tݒ&= WY�K7@��9,p���[>d
+Bb_;�/�/��O�K�ʾ���\|��M+�_'��/D�Z�[1��Ov�*7E��$+Z�C�A��/z3��+���$�OL�����c��N
+@(BX�C���eS�7�i�ٗ=�N���0R�ZX�?5�0�œ�
+^�K���(e����������x�d�.�dƇ2�(����y� 1h�ޙ�Ȓ�e�C�I�u(.L�I�6����Z�Y�U�t�I9�5����n5��N�geR:�`�9��ܑMi*�./��a�K5�Tr ��$ɜ�J���1�&�j#�V�C�����	�a::�?��n@��w���ӂd�ƨ[�
+�
��*�����A��X(���"s�)�wR��J Q��6ͻHڋϏ4�0�_�{�z{PM?�f�#��B��#_t#���'xC�I�Ҭ�U�s1Ԯ%�V��6N��.�
+BF����OM���.G����ݮ�����C�E�if�g"��2Pd�m�O�Z��'c�R�Y�t�8�;��&7x��;��U��z�?L���5	"0i���/CO����V��;;ܤ�:����֫�p�`��Z������Y$e�> �X C�n�]���Sk;����zMZ��C��v$s�b2
���v�W�wC�*u���{�O�I�f�M�eg���WX5��Z��s��%�ݴ��V���"��?2rVL��퇂��B������Zލ�'ˠ8z���_-�\�bڒ�Ĵ;��/{]a|�_pre�#��1��i6�!�
+Nh�MQ����&=��oF��+DV!*Ms�8”�����r��Ju1��.v����a���k��t/}D��@�˥k�X��V]:�B8����m
�.�^��`W�bv�a�����e�ʣ}�6�>����F1ّ��l��	):��oA�����tJb���Ro|�x�f�,�R6�8��w[LlYqiO~┢x7"H�����']Ke\��*��&t9{Z��������C�3�j�p���Ga;����=��;��c#(�)�+�����b��D{��S�S��0s���Ri�Ҽ`vV����&�z��X�6�Ů{�oT+\)V�O�Nj 2�f͘�/�g�|�}e	s�39*G���w�o���]�����LQ�gku�e^�|� ��*r�
+t�>�������91P��ܼ.T���NNa�+vI�	�ͨ�"��mR%Q�j��(.�,'A\0iH�Tj����M��:n���"�
+�SQ�<�
���*�`%��cQ%��='�b0E(��}rli���BS�I���4�9�r(�5�1J%�WP����f9 628.2523813469112523713) �
!#<��D��f�0	 /
58J3��\8�&�Y�p�)
+g�?`�
o�,$|�Y��}���nP�0g�/��晓�F���9����4*��{S�iИk҇��d���ft����V�
�5+oa�[l����:�501.ml10SVGFilter
+/ :
+/XMLNode; (xmlnode-attribute/Arrachildre2nodetyp(AI_Alpha_1) /Unicodevaluidnam; ,of100%whyyxx2numOctavesturbresulnoStitchsTil0.05baseFrequencturbulence1feTinoperator2(SourceGraphicinin21Composit/Def ;44fractalNoisBevelShadow144-2xx2stdDevibluGaussianBddxxoffsetO10specularExponen2(Consta5urfacelighting-color:whtylspecOu-2000zz-1yy5xx1PointL1S12(kk4k3k322011arithmetlitPai2MergeNod1CoolBreez14-xx2(1.radiudilaaid1orpholognn-bnn2(bAyChannelSelecRxx3s1nn2nb3DisplacementMap14matriColorMnn5Nnonccumutotofrom0begreadditffill5dalwayrestarlinearcalcM1anim-8ccccc8cccc1cccccc2nnb1D_2(166Eroder66_4_72(150 5RAI_PixelPlay2(indefinrepeatDremov1spli1 1;20 15;200 200; 15 20;1 1 27dd12nnc1122;20 20;yellowdiffuse525;green;blue;indigo;violet;red;oran6elev18azimufeDiD15rel10011102AI_24dred212315688ta2.1854Woodgra155-xxofloodblack; opacity:FloonnsC2nn351052112100Gray4-xxOCompBlurT1.CompXferFir2(2tableVFunc.7 0G1B1nentTransf�6�Ë����� (�sp�,�L������(4�a������ ��`��0�D��!!�1���~a$�׹����w!"#����e�;����ݮ�\�jT`�L�2��� hh��7�'����4j����C��'��(5(?P�wԯUf)�9��4;o���V6��m�Y�N_�t}8<��k����g�^������Y�k���g)��-)�h��T�����c�S���i��*oj)��1�1.[�Q�_p�茭���(U:�+u}w�t�t���G���W:V�V��⬬χ�(����82���[C����z��$�RK+��9�u�.�׆�
+F�A�=�y�i{������<8r�4��&�B��[zXYj^4>��`�$,J���wq1�Zu�	��z�]J��aB,R��w�jx�ڗ۫Kˢ�B�X
+@[8)��E���|g"��*}�6w�m
+�6K)���<��T��¤1�w��qg��=�<
+��^Ua�	@�76)�0뵈>?m۱��5�B�=��O�V�}�OD��$�����d�G�������ܶ~D���
�,.�i�[������g�=�:��ax�1�E0�
�K�ǔ�^ԴS�7{/��h�vK3�^��־�4�|�j�%P���՝��
�!W˽j=P���僝8b���x��5S],��S���F�Lz0��
e�,�����	�j_�@n�FLf|�7�k��a��&��{'�r~��^Ab!h++7�Y�����Q��7�����\B��#m,��H�
+�AKR�Cf.��Llw�b�z(�wZz����g|K��֜I��cq��h�����;�8��3cƀQI��'�o1�-��'�'m
+� BJ����E����Ib�}3A$���*.�j:l�fL����8d;˜X���{��R��=�&U��%���]��8���X��[�qt���,�4+Ir�>|��2)��P�3�۔|w8;%�.|��(������Gô3�fig���^�
+#p��+D�VD�>4a9G\*��e�Ae�D�������R*����>0�F�(��7������|��Y��-���葚,lt^�a=�i������@��ti/�.�O�@~��:����Q�ww��<V�5���lX[���1֗���*T��!�'@Oӫ�����K��?�:�L�̬����M���;e��Ί����80���@���Spg�;-�2Zȿ?|��]7QJ{:�v���%�K��%���9�.�|)}��P&�e�w�Q�iڨu�<�)e�g,��8���1�M���m���}LP���{�Y��[cl4@�[�6��V
+y���˯5)8��5
+bJ)���ݞ�&4
+��SN��f�a����|�Tl����{���	y�8�t'���D+�m#hҊ��lL��7���N��r��U�|蓦�4���n���E��.,Ta-�:�P�|B=���-���*	��d_CA��<R�ȾtQj����뎆x��o7�֓���[G��N
w�|�BK:�inM8�2��2KŃƿ�=�9�)o`+�h�_�]��ZK�e�"�y��_���Z��qѰ{��"mP�C����.$����d4载CA~p��ݗĕ{&k��s�DŽ�`I�-1N8��-��X��R�n/�f��:�>E5	���y�PL7�A�<�
+`��ti�9�_��(\�{�dZX)�fJOm�g��5cC�{cڷx'!�}����H�(|��}�/��'�H�#N�{j��9���Q��*ҿ�z�z�`4�>̥��W��>�����N�����.RcnL�_�j��-�h5�|�"�v�Y�(����%�����X�!7O��fpb�w�{q��	����F���ZӔ�Cu���gXt1~(�t�Ʊ��K&[��R%p�=1�T� X<�=T˥`��n���"�͍�D�5��m;#r)�=��FFPd�?G���ſT:g�-��^�`��YCێ�%O�Dgo(n�H��������#c_���JD���σ���B%�J�􂬰EjK!�	�X��7�䥀�.E6ĵ)�%d��=�|%?�
+��9Yv}=T���''\1��$�|�����w�\6����eW{���xj�x'{��錛�Z�^u%���
+T�q��%R$��L!�PDŽ���%;�Vi�E6)[�8u�<ӣ��%O�-�3ltޟt���_�5A\ɒ�D�y�
�+7�b�B�����A�M�k�H�����37�V�n����‰g4}~��;�R��jJGx�I]m��g2�U�Jݕ�0�؊uO(T�FI��E�Wc�.V\�׵a��p�YVw�qJؑw��C��\�RA���})�GqU���6����"�`���{�Ǟ�������|z�+��H`���2&\����`�v��W���ˀ��+�b�.čfM!UUo{L��x��_�dӊ��X`^A�c��RM�����_U��vfzs&���]g(3EY�B)�PM���R�v,,�TW~�����O&�ƻ��G��]s���E����˦
C�O�_j��F0��-��L~p�>��ɼ��QY�4�X��}��@���n�uB1��(�"a��q�\���ERi^8�'�x\���)�%FjQw��I�	���bL9O�Q��scq�(�����L�5���[G��$�º��~�&0 �t��5�֒���i,���$��N&q�3����&cd�	U�����Pϖ�F�@�ȃ.t}�������En����e;������1/}�>R���1rO�7ē�u�u���1�8�N�
��x��M����WQ{���
��,ek����I)�%�����N�d�tSn����$1�
+"œşC�
��+1x�'hy^֟5?kcoA�_�w6{ۑ�	�(L[6�"EI��r5I��ɐ�؉����YW���)V��,9�䩕t�H��_S����O���q�WK��T�w�M�T���:RŖ��ٞ��Y�um�}�C�d:5�*}���C�If�����0���O}����[! (��p{����S��%�4N%}��B�\�PU]�V�%E)�G��mf��Ԍ7A��zo�v-�e��:V�)�k8{'Z�1ޏ��Ҙ����i�Q�ǷjW6�tj5�R\A�!��Ұ+,( 4U�:I�^�+��	�?�f� j�YD/h6��Ȅ�tR��M��A^/���'�F�<�I�#��F�9�~��v6�s���
�<���ƹ�#JC�妢��U&��vRH�'�JJy�!�J�/=�a���ʉ�8��h3GA��Y�T�D�N��(��扷�)+)qȢ�y� �6B�(5d�~煅�"�U�죹d!<��]���D��9
���������+�j��w�U��ەAFQ�ED��v�8���pr����V�wLXA�d�@�����̈́��s�M
+K�rQ���]�{8N :x��t$|�@?�"Po�P�7Z����_�u	�|���ӛ{�����6�aS��[��blL�4�vƫ���K
+���vz��y*|��B P������������@t�߶[��k���0�aaI�!A�$@�RJ)���,�Q �FJ��~]	�	�+X,|��睙���mt��4[dl�4���X�g��6��
	��Z�:+�m\�3ӽy��;��	��km1��~�z�~���v�2�
+Y��9�����9�,�+�����[��[������;��L��y�v�ۭoX[l���4Օ��uR��y�l���U�r��}@^ySlUZ�ߖ���7�qv~�|�L�E�o����n�f�~���9Z��\g�W���}�J����c;�֕�ni���z���i���L��t�i��6�L��+��y�/��;�zy���ׯu����̰��^�;�n<��n�p��o��O�����)���? �7��՞��Ϸ�k���z�ė>���uÕw�uǕ޼i�1���v�����l���Zj��'�{�=_�=q�S���O��]�{�~i}M-�o��z/�?q�c�;�;ݶ��O�~@b��\������{ۙ��(
Di(
��"�[-ݏ�������[��o��L��w�׻���;h�?���w��8Sm'ޚډ�s
Vz'�?��?��ߩ���Tc�x����y{��=��ߴϾMN{�f�k�7K��i�4��r����i�M�����vӻһ�K�x�)W=�O�6y��S��o��\-�s���֭i��^���O��xӬ�~�U�����)�k�3ś��P���=�3��n��mf�zk���^3ӽ�ׯ�m�+��r�O��j۶m�N�n\�M�Gg�o��-��9m��{�i�6��}�����ڟ����}�3}�w2�۴�V�m�o<S�+�f�۴��l��zZ�3�����w��7S^�L5ޔ�m�׾r#İ�g�/�/�o�> ��n�c̩���ޫݦ������{�_����g�z��o�6�ԓ��tOηٷ��}��g��6���o��޷���u�G��z���7�8�Kw�j)�.�lݶ���ڱ���6���g��v�߻�Օӷ�/���ާ�����m�z_a�<�q܋�p/���z8�CiJCi�q���8�qJCi�������k]��vbL��q�y@8��y8��p�w����9�o�������u�O�:s�ﶕۖrι�3��k9�W)��~i��bL�}M�Ŗ���rz���~���m��<Ӽ= ��s�ߌ�����q�7�5���ٚ���=��㽯�O��R�g1���v�-g~{ϕ[�m�u���֎�[@�<�G���1�q*(��	�4���PJ�8�Ai(m�a~�O��.��a&P���Hs�b��Q
+��4������@�[��w�;16�6@7�PJC��$���S�}D�=^9z`��";R&Ի��^���Z�j;����Z��]�Ζ�m~����Mw��޷�m1��ԕ���~�|�1���7�=W��������޽�]o�Ϲ~�q����9kN��Zg[1ϖ�S���V�j1�Vs��)�������ߏ]^�z�9������j?����iu�<�͵�o5ל�];��jZ�����5��_�Z�;����~]r�:�5ݕ���/��o���}�Z+�m}η����>���$��YQ8@i*G��8��� FO�:��[���{(C��ԿU:��x�o�V��ģw�|��{e��篱��+1?�������[����y�
+!�����b��i���s�7G�|s�clo����ȫ�jm�����6�Z�������[kŵ�{�o�{�������7ߛsg�;�3�mZ��zb;�շ�3�z�|���{��M�mSoYW}sަ�^w2��)����'����ͬ�6���!M��6�6�ަ����m���7λv]�ݜ[^y��ok���}!��>ur<���ij= ���}��O��[=���^;�ݦ���{�o�{��s���֮��3�o卞����z�x?�z�ժf�m�xkp��l�fN�����������:s�l�i_���s�mZ�ͧ����ڷYS��ӳ��m���mΚ�m��o�Mڧ�Y�^��-����+���y��^-����n��N��ji��n�ލw^��= ��q��l�Z��]FO@���e�c(
+�[}1��:��4��7�6`��:�tQ�b���k�(
���s��? \�x{�ܹ~�g��ҷH7�����	a�����PJCi(
��4���PJCi(
��4���PJCi(
�= /�������uVϥ���f��^Zϴv挿S=u�Omw��R��x�O����= ��w��e�]\)�m���too��n�K��&�t? �����3��? �z7��[L���������&���ϛ��K���ܼv�-Ƽ[޷��{@<��L��[sz-�������@T�q��? \�v�����i�v�[�i�?��^�U����ĚWzmǵ�j7k�5�ۼc��[L�6��&��{@^�Ĵ������^9�}o�{_�w���$��
+�Bs�����GmiϩX�z���1�j���Vى�Ə���Hr[*X�ڀ}B�~-	���Fq�4�I��(�1��?�t.��+�h�� �'`�q�����+?2,=�����)�s*(�4z`��F���ǁ1�{��T��B�������<��s��N�}ę/0T�:Q�O\��j(����.Us�ȹ�ƍ適��ߜ���Οs5va���o�Zc��+cbk�}ĩ�Ez��G�/=�Ks�`^)��1�+��<�Ee�SrbGj��O�z�J���N�Dž"�(k	|c��v���9z�N4�O@��:e��@1���b�k������:c���o�NO�b�&�_hn�>��D���*�`����UCO��_�#}��쓘e'�B��CV*fz����4M�3���5�1n�T]gr$��)2��{�XIzWv���^`���W��B��W�)�ձIX7����Y�"�R�3"7`_Yz�����~��WX�aJ���6���Yz��3ue��@�Ց��$c*+� �}c'�U!Xz����o�R$J+ �N���M*(-��:�#����m��D����N魦3q�|[n{�uo�����n��V�8[��_Mgv˿Z;���w�o��<_n3���}��5K/��~}l��)O�2[MgZ���b5�Y뵽S�?g��[�yշ���7��m��j�F4q�g��ՙ�]/n������g�5��w�}�]��;W��c�q�]כo�6���o|w�7��ؾ}�C����s�_���w�����/ߛg��{����3�ֈ�ޝ�jm���z-י֯<�ۿ�-כ�ܳ�3?k*���V#�:�k���b۵��Z�y�j���3�Fi���^1S�su�Jk��Z�y����/׿��wk��n�M�o�{�zw�[�Ώ鿥5���c������yΕ[�o���k~_w����_�+���{���+��Z������z���>�U�i�U�z;���߮�kD���q}�_/�Y���_���Vܳ�Vk:�F4/����?�g�/��1~\��o�7[~w՛k:�w�h濼�m��rܹ���kw����7���_�s���8ל���o�Ӫ���^�{�Vs�����ֻ9���7��rn�ۛ���o��^}�Μ�n?��Z��|��q�[�޿k:�jD����l5�����V^����[��1��5g��ſ���~��vl���+�\[�_��9׻�{3���33ֈ������fι���;�+��Z_ә��j�����lo����Yc�M��ӊk��~�[�]��[���w�7cݿ�ݿw�_ә���h*;��3���j����o�[g�q�V��k��r��_���Z�j;�4�k��Yә�V�+�un�s��n�5���ۯ��+�7W�����ڻƘ�k~ͱ���]���b���t��׈���nK��ך��׷���o��L�_#��[^k�9��z{�u���<�|k����m�5랿?�8oΟ�k5�i���;�����ڿ�{�t�ޟy���٩ݺom���n�w�:�:��t�Ə��t�����j�i��-ߵΫ��ok��v�h>�<������~��W��oZ�����V#���|�k��wm���c���|���;��ޜV[����Z3�tf�Ѽ�5�rk�ݺ�:�}�-�6o���ݿs��s�[W��V�ؾ���;��=W���5��/�������;�����+��Qdm��Y[Maw�8o\��Yϝ��t�=̎��Ru`�"Q)= Ȁ	�E%U�^Yz`WZ�@�>�hF	�:�����8�@��蕱�k���+�yc���������q��V����|g�S��*x>r�N�C�י+X'
+�4��8`[T��'`S�T�Ș�C�H�\c��S�O��cZ����K+ǖ$+�6_Z������*�Ҭ��]�ێwſy�]?�oL�������]�L���̂9	&�{�H5�6`���=���{u�'��[�b!��\�<��x���6���±�ٙ�η�]q���:������v����׫k~[{�u��/�:_Zo矿r�v���۝5�;k�����Ż������[zm�;��~-�zck-�un���?�8�Mm�y�k����9�zu��w�[��x�zm�_�پյ����f����/����9�
+�9ߚ��i��������^�az1�s��}���>}r�`c�[����	
PY}dk��DfZJ��b��'��f��PfZ<x���`�W�T��@��)X�<��j�y@!N��S}s�4���o1-
 ��2 ���/����r�J����YP��$��
��\�X�T�u\��4����N�����L�׵<+X,�	�J'�L��1d��$BLX�B�B�!���E�Ygف�\��g�A�B�:�Ƀ`�R�%��Ԉ3�̴�{i��N<k���;����*��g�uf2u֙�33m��T��̫�Ls�Ze�q^lBL�Rvdl��O�Js�`�Ʈ�i�-U,�(;04i)�\f*�6-���)�2=@hS��	��A�r��2Б�Ʀʖ�LD�<Q�l��ˋ�I�KbR��å�eG0
+x�
4�)3X{@j�D�
��"t���٥��=WÂ���>�K�pv����˄���,Xn,�����uh�TJ�}����F<�v�oYL��X������wm$X��!U%>�*)�
+5���D�j��Tj*�ԑ�V&C!�"sK��LFB̓ɒϤ���U�d@�H":*`���D�@�d�-
+�&��,:Z+6b[끺6�ą�v/�iY��&����8��0m�\��&�J�@WC5pKR�0��HЀE�0�Bv�#AU�d?DJ�$�FF:�8A�����j|0�HBr��ذX0���X`|��E�.^Hq�‰���	N8�t|0�Eb1�L�G�dvX���b�Y�P�"�Q���\�d!?"%3,�ۖƓ����Y�];`�VUwqQ��`��C�L�@��sh*���Dg�J��P*�r��*)���`(4�Bj�2(b哠l.J���aD��B�rQ�P@�'L��xe� '	��P�lb���	�j7I�*&��	�� MNZ4��
+�C�X|�`R�x"�&t�rQ&�[e���%j%
��?����3��2B�!�m�,���rh�4�v�̈́�@�pJ��:��.(1��1LL4���
+2��JQ Fj�Z$�����(�2"*!�!��$�qn���?�L\�t�.��+"�U�%��lx(�����gBJZ��D{@8,��e��$cW�$�cH(	�d�p@H��EI�qIp�P5%H>�
+�	�@�@Bv/.��I�p���m�$��"i��D`4E$:�RF�d�KbC��"_Ď
+X��JEP�	����HAW)"�2"s�"��j�4$\pp�1����P����D:(F��AG�D.`�
+��G8rD{@^�����T#�E���hU"Ig!uDI$֍�H�(�	UG&�
a�AA���ĕ�]�ʸ͊�	����pJeB#��2�1q�iHK���@�Dም����a�Ȕ}��C6ғRf�0d(��2Nl[�$b�(�������	(CI,<��B��0C�64���Aa�ɶ�D�iH�RdYA��C=�$����Y�d@g
+�����J�ͼM~�Pz�L�md2˖��b�N���q�d�`ᒅ&�%��*��P2��
�I�����0V c�H2�I$�dY�i[�e���D����PyX	I�,�0*ـH�]:�%�`�d��%�eYV*d�ge��dG�*��TI��E�
�!�:�{��>�qN��A�l�sD,�`ZY��	�G�!�e`t�G��sYdf������PF���¡oG�%�X��j
+Ԑ@�
+Ho���Zȸ8:!Vh�އn.^[{@����!�8
+�=�L������	\��@��:�@x��$h>�I��u�(d�m�/N�k����t>�������/:��D8i\�&�c�V�d��,ɀ:���硂�#z�pq��ɡ�F�,C5dYV@�\Y�e,P�lTȲ���dY��d�*\V�%�0`���`6�V��e [A�A�OAk�Q@���'�J`!�;��V&�`��f���GK��>)F����^:��Fv@pJ[�s/ܥ��� �)PK�z���	���@�}L�l'�,˺J�F�Et��'�-�2r{��<f ��ʲ,cȲ�2�4�C$�2�e���RD���dY��(q�A$˲H%�2�:p��@U��pP�o�@a���Q(Y�Y:0N9˲�5y�dY�eYn`lD�e�(�eُF�eY6��e�= �J�E(�,�hX�l��"'��Z1�>���z��@��'Y�����r��ьZ*�	
���l(2TG��c�,˴U��>��F'3	f'����*����{��-�GF@ԯ��^��Wv��Dt��5v�����+I�#�B.���J���`������^��Qd�����e�
+G�]c�r��1�W��4=�E�!l��>��wJy%م�oT�އ��\��$?V8=26����&�͖*FNX���,c��8�oΰ�+σ�X8�΁o¾nÛ�p�:P��p���>%X7�nz�ǚ���0��I������H�Qy�
�1�6��k�H�o�����*a#T�S�?��~T!,$M��'v�
�W~+*�)��80���<��ڄ��-��+=SqJ��6Rђ]�o�rIv�Y�Q*T�u`J�\$M�4rx�z��	�F�
+9@�{U4r�`��3K�W�������/@I��@V���[u-����AJ�%LEI1#i	cU��"��4��Y�R��
FIqc�@ 
+��JX�<2�C*
+�@�b,�{��q5��QW���B�Ry�dy+�zmX�y�5���g�o�Kr�Q_o�a�;�V�k%z �0<�K��L譾S&�I�K�G,�T�1¼o�����5d�>o�k���*EGQ�|�tE�����ʃ},�S��(�Q��6F�U/:����jT�M��H�*:��W�B�;p��t5�>V�R�����Q�*�+�jT�<U�������,V��<0E]�
q�W��h*��d:��	���>�3�ƙ#���	�f��T��`��us�8�~f�S�@ϔ�~��+������#S1S(r�KM۸a��S�(�B�?P��Y��#I���L��>�Y��Z��N�����@��进�q��3	�[�Fb��:������E��{<nP~`s��O�}<0�A|F�W�F����Nr��Sv	�g�̂�,�w�m,��!�4�`,4�*����n~%�����
+�+���2�f�'�)�����
+=��±�R}-	r�J#���q���(�6`_����1�ߟ�,�{�-J�T]�dy���D!i�����Ĺ|)�CNJqK<��(h$�6�
��\�znl����0(M��:eA{@L�P�g�,X{s��5ѱ�������	Q�PPP��)25�D�B��#jB���XIK{@���b=	7��LL��3�&6�D�E�E�EDžHbB��t?���q�q��\\���AaU�AauPXU��&0
1b����Sx
+O����Ĺ��8��xȰ�����00���qx���j�����J�WI�1M�����4�k$Tv0�Ϋ��Um0Z�8���fA\�"�Hqp)�qp�)|E��U^���p;��)���K�7|^
+�@�@�*�bb���<`��a�O�`��ª#Q߄'�3z@Z$�%A���2��J�r0�t�V{@F�r�İʪ����L�6�Q�n�ƣ&:��T���A��h�DG��`6D}#��del5ѱ�QV5ac�گ��t�Plwm0
��AĻ��$���ؒ�l��sy�J�7"
&le,)ҽ�2�WȰ��!-��t?@ �21���BQ���^ѱ��E���t��
�"�
+�x�x��D�m���'"q�bb� 9l!���eTH���P���`@Z(@���zXuD�6�@Db�>�p�^�������$�H�4_&B�@n�@�j�g�K�����`�?�/!C!i@m6�M�t����E��Z��2ہ������XUč�e��y�L⤴�Ĵ�FB-E����6�vȰ��d��]�����8\;x.�3��
l�	g�P�gB�7L��T���2��Q��f4��8P�P��=-0<t��AK��EH�AQh|�&ѴE0,�8�XZ�j�d2Y�\�&l�@�M�(Q!]1!/(0,[@{@$�	4b���t‚�16��T
+��2�Ba�!�$p2C_
+y��\�f+i(62>0p�\ޒ�|	E��PW�����RfJ"��'6�6a�|L
+��#���x����$d2C4:��7��"n&��T���6#S�
I Qك�d�P�t4|x7�N�j��N�7�彌��	\ɨ
�E���*Z�$ �l�:.:v�I�P���>@t@-S=�~2q�b
T��6����X� ��D	�i`e��㤴���f��F��u��J�8���dU/�	���'NV�TU���qp  "	��Hl
F{@b�ɪR�G�4��HC Pu�P (1l
+%P��9(����J"LV���5h>�*CLɒP�P��ڨ�Kwm0'����ɪ�x�`8�U
+�c�d.����(��.b��`NFF=��
+��E��0*U���$C3PQ?IA�#19mY�D�\D{@6��S�2�W֓��lg*c��b��Eܾ$S��1�t���`j�jF*D���U|���Õp�}��ME�u���#K4���R�(.JD!�=YU�)�ҪC��AT=
+�= "��4Q��Pu�0`��B���B�ZP�]�Ht����aqP�h��ɦ���AHaℋ�!����*$�����I�ؕ�0�2�$ēU�xHlAE�b™ʠ`1F
+P�s�X\
+��;��Q{@&hjDA�D��
+��jP`�T�$Z{@>��`y�z��.5T'(0	\���
����`�!�P4%�������"�%	�
+�Q�KG�ИZ(�Q{@2`��O2��ܥ��`�d��U�T�&����p�.p�b<��Hr�ɪ����d$H�����l(�C��H��������'��Ԣ|CB�a3Y#�zJ
+�bPl0^C��6��*
G��U|j��:�0D%���
+��'�ٌ���x<TB]�+�Ð�fP���_޹�����
�DZ;X<Y�Ph2Ւ�}&H�� ��(KÒ���@�+,��k�ɴ䁒�2Mµd-����
Ee}:̓$�%�O��8�����[&So�:�l$L�fR�����R
+���q���OJ{@6�,�B��5�0(��ћ��n$ё7�C{@2L(j�L%Q�P�h�I�B����t���Q�.��Qz�Sٌ��d��ڼ&"/�c w����A�6�sr��IwiF"�sa��J�C{@(� �p�
+,�G�:�2�V�ᶲ�tm�&���ѵw�"�\�؄DKՑpY��ThJ^�
+سX�^���VzՍB�œ&����J�zH]�T�:�J����Ci�L%��N�p�]�F�����p=̣a��5�B���P������/�U�5dHt�36bp&��C���L(F��S���T��&����ظ�����	 *�A��
+n-0(R�-���P�\L�F���mX�DD��
����`S�"���N�M�tzMF>�
+ �SE��E'BrR]"R�r��BP�08����H!Z2L����1�2&����A��6�=x��L���1,��*��!��C���9�%��8�$]˖(.PG�@,b�`kƧ� D�
���$K0X�4����1YA��d����mH��-$�S��p(&N{@:��(F^��O���;�a�Q�Y -*��R�hA�0o��`c�Q�9F�EĈ�b�6�
��J؀Z	���I�,��0Z0"�J�i��2e�-�4�LD�
�P��c�V� DrHPB���d ˅w@�f��H�Ӓ��Pڠ�!�g���D)�G�,M�
g9�{a����F�#u2:c:��C�p�(�VG��$6$8+��.Dž��qD�k��i�nBFԐA�8|B�A�0F�́˳m�7��I������H}4�qA4��YA�,�PU�c�XV���Cq�i4*X
+endstream
endobj
20 0 obj
<</Length 28801>>stream
+���T�����A0ȒJ5��]Y@Q�Xp�����D\����@|bˣ D�H�����p�cxݠr��i���v�� �g(�Tp��,p1!��K4�6�I��}	��v��iO,d�����V��5���	E����43�s� �N!E4"��@��0x,
+�bI�:d��jV2�P>2�B�@�#)��0��B�d�Q���,��vCCY�as�I�J;������f�������.���L��좳P�	���U�(��ʌtT<E�w5&z.D[Іc�U�[�J-4`�u)a�l袊����U�q��/�\��O)2^U�z.`M�h"~�5�>/X�Ie�m��0.
+�'VG��T�G]���m�s�}~���;�͕�����2*�o#䋠������'}���R�2�7&�/���_����H_�x��t�_L��I*}O	���u�|C]�/)$ş�$_���K_
+j��w঎���{����%��dD��;�Dk�A�.Y/���D�e����վH�B?\xS��W�nѾ�3E��XR���F�t���|*��:j�A$��ktdP�6�d���D�|cYȩI�6痠y�b	�f/�����9P
�zH��P]>ҝ��u���a*�U8�&��/2��SW+�~���0Bb����0IJۊ�!̝���VO �J��P�?�u	�� <ˢdd���P&0ۤDw�HE��@����D'�����\8�\�[\�(Y]�cI$<���)�-m%�+uB��O�UF�>'�R�E�m�=��$�7��
+x���~�юC�S�(�R3��	�Uw�Ӝ7���/L ٍ߭��(%	uW�Y��m�H���P��hݺ%�Bl��`X��f����?����[JGg���xޫ>��t�5���o�>� �.'x)&�Ė� �{�^榽���W���p��/�;b� �c͡��&�9��	�N�V͇�
��(`�Á}��Y9��B���'�ϳЄ�FDY��hw Ù�9��L2���I��!YC|B�.$�7#3&m�X�2jV��O1�w���B��O�J�n��;�ϡ�I�M6Tz`��ޘx �
+s?k�i��Bup�6j+���_��ᨁ���5��%��u�f��!�,���A�B��8��	�����TQ����4�=(�d�7�������s�.;�&c�!�v����gy�&!���cn�!(VW�e�VB��WW�~
Ys�)QS�͸�е��4�To��6�����zp�Z��R����y��][�[�����K�w�a'e�0�)���!!��7��ia�Kԣim'�Z��id�mCN�����䶄�`��u���������$���W(�j�i�
+���/7�=��3'#*�*ִ�NKz��C��b�o��֯���! �P�b��YV�����;��T���JMa|��+Ç�I�Hz;���)�7�0��Oh�֑f���@ɠJ.�ٝA#@7�U�+V���wf�HT��7I���r�[!U�B
+�] ����诩��|�F�j��s�/Qq醭�F��;ot݀���5�D�o��j�q8��ă���+Y�ܿ��;��F7�e���D���D��9���������}��tZ�K�r`��'!dܝL�*��p00p���,ј��Z�Jf@ ��j\��$�T2�T�V��?O'���D�X	��nՍB����?;����M�v�Jŋ~ ,a�m�N�ْ�� ,��&��f�ו��ROx����΃Mi'����,Ff���V|j��B0D���ߗ������pk��+x{6LJ�M�{�,�Pt��禞0�h�����k�̞���BnB�m��l�}��Hʕ#��$�g����>`U[%X''	&�O��Dg��h�%��Rd�$��-MY�w� u�@T�\����
~�6��7�#])ls�����P���)�Z�B�@�&��eɸ���&���Fx�U:T
+�=��h��@���l��JQՀ^�|}���c[����������Ǭ�pk�J�=؞ѵ�č
+A٥ON��8��c`�;ۮ#��pB<��5$
+A��lF�!k��QT�����^֕ȀHj�3���R	P~�,J_�&Lڀ~je�ո�b�f��L^9g�*(�=�w���3e宁L��c���X�-��A��������n|	:F��oʶ
+�D^�2�N ��Lg�l�5��#��\��DI��}������*C�ha����判���x�~""�n�ɲa��(��F�]z\��7�E#�1��q��Җ&�)��3�P�������
AL�h�����K�+*�,:{Qr�N��%dkq���y�C.�ݦc.��P.
+y0hY����z.|�B�U�����FS��.����0��%m9�������Z��=#�ٍR�U��� ���ya�L�:/��Co�� /<�?�s@�ϩ\���m.ݏ>o���(�STB��w��p�y�ʮ>_:1�t��	v�*&��
D�����n
+3��xu ���F�n�1!���mH�`��)��<W��y�jP�rvM9*�f߶��������c �s�����l�6e��?J��xi��Ա�'�`H����T�p���[q@�����|�T��Ϸqr/�M ٱ��\>�I��x��2wf'�-��G�3\Y������{[l��`v�r�*@��m�a�,m�����8]�w[M ���\��꫶�P��5��eP���ǥ[@�,Z!�/g��|�|����$x~�0�:�(�j+�v��!��J��F�fpf������#��;���������6����
|�1�û� �����
+��h��:x
+�
�K�u������.y���#mK�rUc�N�V{ߓ����A�:��
��������N�sX�A���4����S��Q�Iި�Ape��/�\��5�W�$ʝV�S+r\/T�N~�����z#�����n�H�AzQV6�\�xX�a	�b�E�E�XX�-�
O�x�B����j��j������$۾`Uۀ���,K�W`S�)��VB��uꒇ�D_~ІC���ܡ$���
y�W�-���5<��B��͵�~���zj�f��,�$����R:
+W�$�F�j���T���9N[x�0<k�>9@�<̈́���мNQrC�̀�n����������8�ω0ĢK:5�'��Cnug����l�f4��6O��p�5��}q�����"�1�E����^Ȣ��Q6d�c�Z烜m���JA�/z�#��[W�a���նV
d����f��:һ�ɆeW�y��1L�oh7H'l�Q,��y_��%��"P�_�,D�<o|����M�|:���	%���v�W�kи	�o�(@�+@WS�P?r��tw��W�e�Tg�K��
�(+�R�<>���(̗��9������E�hJ�y�����6�hb��F)ı@��D�y� �;�Nn���3(d�I�M����~oh
\��"�SSI��J��{~��܍��_��errY���X�U�~���JH��*�Q�����Ϡ���1W%��_.�Z�"�MN_mȉ���K?t�m&;1\=�R2x.f��"X�b�Zp�����h�d�)���,��1A�%4�$Xr�t-��"����afC��Xm�#V��U�/\Ԗv�}A�,�~�YF؁�{"t�
Q#�ߵ�6d��)W��o��EY��ԛ�.��
+�;��),{j����?�TJl'+���}`�g6������Oj� ��{Tlt���w�`4�0$�a��Q@�$uC��:4�M��
�9��ǀ��n��4�&<ҀY0�J� VI`*
�!��[ռ�ody߆w;���
+��K�y��<��esR�f" KF�釆&�/�C&s,f�1�_�<#}2i���.t�)�b�@����LA����O_�
���G�@��	�Aʩ�=���5��覈�D����
�U���'����ݕ��Zװ�$Зԑy�3�"�*:a�	���
+[n�'�p
�(���gS^d��#\ʣ~d��5+��ը��0#�N�^�gk����T,0�"57�&�f�ކ!6�Զ���Iœ�S�l������ņgb+.{�W��^�>��`g��=�㖫,
��w��h �A!-Z�#�~ �iy�I�>e.zp���h<���^�1O
+�UU?�C8�C��"���C@+�1�m4�u�6'.���o�?��X�l�c�곬�†ɦ�؎�1���Q��ȶ!���7�0J�N���f���"�y^|D�f�[I��
+9F<�?�$����I&���l�|0!"��)��gy�w��-��`8��=�8,A[��8�QĹ����/)�h�CPX.�cj�t=�K�3�1���FY`A�v���vha,�AF��Qt���	�f"*�Q��#fJ#A,Cc7L�;�hO5#�.i�̫�#iu�4�(���0R���\�h�ƻH]da~��I������z�W#�R��UC�s�7ض��WVL��������0�
+�Ȉ?gd����[Z����1�ܾ�n��Y�n�ٺ!��eC�t�i� ��J+���o/��������M����S�[	BU� h�6�\d���#BR͑-a��6�2��H�P�`0�b��wx��کf6�:ԍh���X���h	�!�ʩ�U���
+j��WE:�eП�	�|yޓ*�ӭVq.O���(��
�+T�7���7��61@�D4P���#��Fi�8+������|DTYE}~��zE��RKc�ȯ9���<~��v��y�橌���z��>����C��>��Du��������V�wF1�M|�<�o*Ci` ��.C��uӑt@���LM
7'�A�"��?)|&�e�$>��\ɝ���1R;�4x[�p+��t�������o+�D}����-�j}@ڼ	[���e^��bDJ�t�]�}o66�p�.Q>+r�luڕID#T�)P~�v��Kk���8���
*4��
ܘ��5;S�;�7�$i+C�Dk;9�[r�$�����7���A�fA�<�<aCI�!N��Ҹ��G7�G̯�������x֊j\L�j|]�8.�ղ`�Ѱ��n�@���5Y�]7���Rg+�ʓϚ��� ����i�G2�r��4���:�p��͇Lu?_e��D�DLySq0�dZa���"LZq��%�Sv0� {�$$�P��c5|���Bn����P���}F���K�[ʢQzZ�VxY�1���.����H3ş/�ƭl�A~�R%��@
��3�����W/ʩ�T��1��Tk���϶�}7���z��51��yq�]83$Gm��e�Rl��D]!Cb���YY�2�V����'˺>;%��/Bո�"��Rho����e]�Jt�#.�z�.���xZ��^'��Bǫ� ���x4J�����3�����6ǻ=���� {�N8�ԒQ5��I���4�؉g^_��A�2s�w��t���[e�!����?��P#��f�L���sG��*����GG�;�=�y���)s���L���}FLAA���o���c�V�g�6S�
n�m��3k����Tٛ~�4����.�e.��p�1kF�H�=fz����.g!�XF�P�k�ѣ�2.X���[�k�s�'ni��B�b|�����S�R�*���#!�(�搦ټ�=A�Y=N��i�M?Kމot��K�)�:��-��ju8� 
tz����1K�Kvn���K�N��c(����nQm�ago�f��4�f^�TU�  ��m���Z�w�xgrM�K��’9mA���{\Ծ1��_[ǎ��d��Uޢ�&\�d <C��¦N,�`�&����B���3d������w1_�g�Q�β��晴����t�K�n���\�ȁKID���c��O_J��XP���`���747&&�	��^{I�&��(Ձ�E;���&�JM�Ԭ|f@ rͽ��o��8PA�K�f ����|;C�tB6��q�����Q&��b�_�sW�S8�^����l��n�!������/�w�T�֡���ֵX&���K�SX�D�U@my%��%Ռ�,v"c�^�[9k��d�#F6-�$6Z��7�ԬO!97�U�A�)�`&ɗѥ�dDW�e@u��Lh�V8�8���g���M��I��j���ԝ�B?��
+�J⒁$ކ��s���uV{�Q|[?�s���P���]�Q��ts����4�8c��*��[�)$X��D�g|&�P����)�}�Lӽ8�"\�?�o�b��
+u��
s#
+��v�3�¼�\Xgo����9sD`h&$��P�L��c:��UX�a��3v����;׏�0�)��'��;�vd���\����Ͱx�r�14����N��*���X�m�m�8��Jme-�@�W�#f�uv�T�泥�02�}p�=��׸�����2%�$^F���z,�=��I�����V'��q���'�E�ȡW�ҋ2I�r�G�irt'�R��r?�`���Ɉ�|{T��\�5��Ǭ���o5�`cY.�\��פ�a���1VhG+[��V���A\��d�#:w��߷A�t�4n2-��	�b�G�����N4��4FF��_AOK�R�:~�M�
_S���i��/�?F]��"�oCY������� �@+�7i��Y1��h+��>^Ohd3Y{/&`�R��hl�XcE���#�=-�CFf���cc3�v`�T`�,�]�:��:g��шE�g	'�5oY:q���P)��s:���o��I��!��;����ϐ���h�=�0�k�������\}$��bR��E_�,)��ˆT9:��I�؂6�hz����M(��!����9�3k�,�y>)ъ��/C�O�T&��1"s���v��g�
+#��z��e�׉�W/��)�O��sT���vܐ͸�}32�)'����ȗup�jКN�p}@Z�(�N]�8��]?G_��TӢ v����R�S��Ct�+r[�ʮ�k+?<���3�Q������q(B�	�}�s@�qZ�:�7�@Jy~*0�p��k|�9�*צk&�>P"�\+fW
%Mf)s6��g-���%��2�c1[��Ƀѵ����y-�{���
+��Օ��1��&���P��E:�m���{�~gZ����v+r�ʯ��m��1a�A�,5q;�|_�ÈH�&���]�lM�[f�n�w��@����W&u�t�$�����]�:|P~B��@.�3�.�pjV��
�*B1s(nU8�����)!���1�И��j��]��Ǘ�\����^���/
�������U����m�i���a��~D�q�$Ŵͮ�����;��1��1�K�{*ˋ^�y؜�-�4�1��D�$��� �~��B�����c]U��^�^
+��I���h�.E�{X�퇶���3�Nh�vLt��?�t���Y؇Yy;����k���3�����Q�����Ğ���Fp��
+
���������������@�X����	��I�?�N?�t	���
_�Z;醉C+^-J4��N�J��€�$|�ཷ5��Jħw%�
+0����W����R�5� ��<xL{l��j؉ך�EȚcl=��"��!0�\h�i�79
�C�r�
.&����錨����G��[�� ZM�����~��QAT�������1�X/����7V��!���OeQ�/��j����Y��U�L}y5��;j���"_6	GIIK�Tфƈ�{��x�N�C?g��6�A�|�o{�<�Y)3ĆE�U<G� ���d(#��F�esn����R�Ѧ��_�L׌w�׋.}o)'��	�FP�����0����K��])G�ω_�	�Z8��H.�Ɂ��q�2�x���لa���V�1���|�R�|���ioG�LVvry��Y5e�AR�XB�7pV/K�}���zٴ�z��}��+�&�ln��D��4L��ڛ[������o�d?4T���h=Z��d��s$�r�<�r1�;����F=?y�
��鐤<��D�o����q�Ù(e��2��=�d!ϗVb��zǴrE�R�&-�'(��{9�6�>��q��̯j5Y*�<ԇ<�8Ş�fPogi�I*\4`���+�0I��ނ��L<�A�.f�`&lb�O^e�)��!!`�şڈ0�^�MH@��F���p���B�n�Oǭ��$3�
+��.�+�A�E�Hn �M���H~l�k\�\��
+�{D�I%����,.�b�x�hFM��iA�)�)��!U}�T�}�
+��=>�P��r�.pm��N�����D����0�AαAF����!�,q�(;�v��Tႅ�X��r�%��~k:�!0�*$��M|�:K�����^��܏��
+xI���q����5�:R���-������L�
�`q-��5*d�/o-[�(�2������w��%��n��h�Muד9��C��l4Rs��8{�%voa���i$���0��{T��rl�8�.jh$��/�Y�ܑ.`?��n+mn�ML.�w�)�q\�\�{i��N�m��Kޚ���>gܞ`7TCӖ���?��/X�*k�Y�LWbP�u.�Ь[`)�U>l���T8r�C���r��S���bό�ɳ���Vp&�/bY!�T��t��A*�8/~A��Q�j�E���ω&�l愙��)]Ͼ��p	o��&�����'�p�sS)�}QW�*(3!�|l-�� ��tTI���2V_�X$���J^Ӌ�-�@:
+�Ԧ�ΒH�w��g���$l�\-���c��e5�R�gqM��=�7����NMˑDw��t�)0;���<9��F#4�ڔtz{���[B�ˆe�K�2��*-K}��؀U��	��1�n�v�N����;W�[%;�eE�Y�c?��=bT�d���\���mf�o�CҤ*m�D��07�|���:f�� �葎�n�a[
+�bW4����:�`C�����g�s�=�Z�G�(}
ۖ��po�CoG(u�H{9�3a��]�$����C[9���\�� ���E9+яŞ"p�73��m%}^�MJg̑٦/��V-����!aݷ�@��-	�W��5�yn��%R���yax�z���<_q\S�Fi1�-�Hr�	X*\7�/w�N'#rr�Mײ_��'KZ����	�"O �Բ��3Ī���,Ԭ��0t����U�$,���v'������B+�ɸ��pa6Ԧ��Ir*��~B�Hlӧ�R�)��%ZM�6�)SX.����b!�Dl�EI����������sn�v�%��;I`3#V?ޞ���^�<Sp�(�pas��t:�I�����2��G<�#a@��N-u�����C����CGdDD���;l})o��]��h��1Ҁ�����-]Ki]e��\Ԏ�jbV��O��X�ګ����"�J;n<6�
�ζ�۟_���締wy+;�sxR�R�m�)�H�1�Y�:ب�1��w����W&��>Y���S����Q�l�G"�����kU��V9�x!a��Z'�.��XY��؜H�aOOE8�!��w�J�v(�s�|rX��!a���9�1�C�HW�L�>�.M��8��GÂ��[��E�K��P�W�&"�i�sE_j�s���w�)�g�@���O���b��"�`��Yz]~���xC�b��[�hJ?� S���L�-(nך��d˜�!_�$|^�(�Ց��l�̒�D9����(݆Iѝ���B����LH&
Q#2�.Cz������u{[��j�&Z��A�YS�%��IWh���!�	P���H������t�9�4.��j�|*��-6�~ͽ���V��zC�n� ���Ġ,o�(Z6�φ0?��e"��o�+XJ�:�
�K;ghqO.d��_5�1�ċd[�x	"� $�����4|�������{`�����T��JTm\<i�ҥ%�ra~�f�(��g�����|({�+
V��G�Kk<�M�!T�W�����h�?5fȸ�Ӭֽ�F?�W�?KLu�9����6y�P�Iw��l( �+}&����x��[�qo?g؂�pq�����R�_��{����v��.��Q&^��"�e5>����r��ڕS��u��m]aKHCw�^m;��9#2���`0耺l�k�L�C�4����[�dl�+�؎>�H}�M.��QJM[0��D%���Z�$��+&�â�x+\_mOt��M�_7u���2���]ID0��{.�x
���Uٽ�Y ���P��@�D��J�'ez��r�z'䋵Wn�\�t��m�@�ܔ���d�q���#*��Y�R'5m�V�n�y,1bRѳ$J����"�y`'����cb�M�^��o���C���w�(X��}{L��_�#"+�S��xŘ�B���/:+���)�U�úT��/����.��cX����`U-p��R�ъ���Y��U'�4�4�u��?fI�����N������%�6�&�D�<�oѥ���R�a��hi���)�'�������V���s��a�q��|(�e8Sc'ےA���F����5pl{ل���*�����e튄
+���F|�F�+����:�L
+�{�ZF���Z&Sh�T��LFEv���xP��`9�"B�\��e��u�4Kh��q���-up��rt�m����O%7[�8�����H�.@9U{v?l��h6��͖MA���|��	p^��x���^xU$g��nQ�u�9n�R!ܤb�j�ҍ200"�_��;��-˅��nn���l��.���\�Ь,8��l��	0�S�p��xB�p�d�P�?��AJO@�:X�7��*�-~$J�a|\U��o��ﰙnl'㚄���Ps1�1��r���g����rb��BJJj�wu���ëR���{Js'������z@�Uł�������8��V����h������?�x'G[Ə�U�s�ț����P��+4zÅf�YH��q�p�:=O�L�2ߪ���.N_�3�
+x(�ۉ���$�B�z:d����
+iK\���QOL%�Z�ff�F�̙1y��&ʝ��
+�"ZEr,�t�DF�Ț�^�e����IM��pP�}[��V9�J
�Ʌ��L�������� 
+�A�����*�"�����ro��)oP���%Z�%M'��f(1���h��y��|]WR%}F��]-���zB���h?���c�1	��Q�O�%pTx���Jl��_��wG)K">܇���%�$�C��&���~D��Zh��n~E��c�H�d�QƠ��cR�"L��r�Ԁftɥȫ�i���8&S
�>8 V�޷`[)����5e�����'�9{��k
+ʺ,R#��L�.Jt؜�I�%K�e�og���uygNfB�Œ���흅����)��T�f´0|�8�7k۾���G����Uӛׁ���:�����c {��Op���i?�����E�1,PY�E����<�Z��{�i`!�����P9*��A��L7�S<"��s�)�B�2�J�ɦ�f�O&F�n���Z�H�F9[�z�;�ϓb[oc���q���Ĕb��y9P �&�燈�n�q_ɡԀ�i�]���u�]Hz���uO���K�؞-�q��F�4���MZl81�E�^w/���V���O4����;���.r�mB�{�$)^���%؉�I���9�[���?^ܶ+v^��܊�I�����盆é	f���D�Ĭy����ѝ�|:�g�3����:����?�� �+|��[+H�s���s^��A��S�?��#ᜎ'G�–4ӹ�&4R��myVMdW(묕��[W���P��N>6�,�ܥ\6�\�d`"?t�d������>��ʵ�~n[����h\8�jUAC��q}�_�9����i�k�+�����_�1�O��Ôc!��6�\%
��&�Ra��¸(׈C��)٩��r#rp���������9�'�4�|L�|��,�9����7W
+���DŽgX�~suֹA#�:"ߺ��S�s���~>��A�ai_-d����Y%�L��j�x�Ufen���k/M8_�R�&Q�K��"��
+��<B3W�ψ�����B��� ��S�x�g���Y�!f2�^�b}43��J�[P�5�qYݳ��h����"�i�k-��`��ݔoTR,J�Rl���6���/n��rҪ=@M䡑B������
+�RxK	��*�,� �rX��u:$����ϱ�T��B�ib�&f�������݂��y_��,a�}
+�"]�BY�s��3듵'��MB�HHд����q������[Mˍ�؝p��	�Ǧ�9�
+��>����,]��v���"d�Z�?~��T%�����c���#W)s��{�q㤼�m~���+ݔAV /�ظ҄N���~�-�������l���.�sc���ASY:��=.��ܽ��W�<	��������z7�+Dt�����ܿV/��z8� (
+=�`�������%����Xf� �cN�Vf[��YU�D�I�� Q:|(�q�^������h�!�S��_\a���I��{�/FksA~�g����
+����xY?N0�x�4���V�/.�n��\���N�b�[���A��d�����…ap�D��DD��!�sq�
�VrZ�nÞd̄�J���M`�P6�S��0�P�"L�BmYQ��Ȩ���@���U'1S�v�����S���g�#I{�Mfr�ms��HuW����L##?�������4�M���L�1[��1'��Mar98)�:�/c*":mRÍ}���o6�=�6�m�"�}���u��������$�I�%�EG7nh5�Y9����?��̬��&B����6y�9.��^Hg�K"+E��[2F��%���^]����y�hBK���(S� �pY�%&Wiz!d�j�}����Ca|�0$��B�Q�J'��!
+����^e��u]M���)G����4�o<Dk'�Se��Zc>���R2���7��1�5gY��tQ_^�]�N����)���1Fz�����-j��K�E.����d�
+��C�C�,t��ߍP��@Լ@p����4�����tE��FA{������u���4�U��g>�P[V�?�Lm'P$�U$I������6$yP�jW���EO>�U��@��b!/i�T€*������cj�W������WD�(��@U����Ě�f�T�R�55550����������`n/tF�T�Rx��'���f�N!3�:����9E����`�ܣ�|��r���MI�5mV-���~�h{��h�3p�V~�p�.�šl���;��	D\��K���k�,$I�(�J���_�"3ɸ}8�� �u�]�C2��Ӽ.���5��n��
+H�����C���ZTiz�w�y|�̋�έ+}�r`Yx�=+}�4�9�8��	���s
+�⎷�+�	�f�?���V�ʢ` ~'�G����(�y�{��l�6����9ǥ+�"P>:�D��A�9LWC>.�&ўI�'���&s�i�fS��X��`0q�MC��W����m�to҉���7(0���ܸ�b�"�U��@ه�7h	��9�}~��d��1<5�f`�{�%����;��Gd�@Q�vP�
HӷL�d���yaD�7n��#$����s؛8(?J�$��-M�R��{7��1��l�OL��&rQ�"Z������Χ5M� �
]E��0�O�E9��"Q�*2�ނ��C���65�½����I��S����5���XD "f�Km �B�f���������Z�:j߈砅b;����>��n'T$��E�H?P��*#`�#�D�$�=zJ���%�p��%���IO�_#��@����q���"�.;A�l����K���\d@ke����
+��_�m��L@Bvw(���t���m2��\Ϧ���z��I�7/=rF���c��p@�㋪�Y���'
+A�L�Yh����"���s�R� ������D&)����DFd���`#B�Y��{�{�� ��.�>5��g�������lP�
+a!���w�6�}�4�3c�y��H�i5���'�"n��VyS���!<v�I��Z�[�2v�I���j�)�S���O����'K�2�,F(u��v�z��6(���\ݣe�����:�
Z��%��Q���w[��=;���l���\?e�H�����.��
+؞ɷ��g��FN�ŸX��F�U�*�N��.���ܯǭ��U����������i7�P�^H*�����Y�O���z���RƦ�UlZ%n򭮰ߒ�ũ|Ă�;X �*$
+X�G
�����q��-B��c�ׇ&����s�	����˄l�ǍU;�	v�8�R�Ԗ!��u(�a�'5����5ǧ»���Mv$�Ɔ!�l
����Q;�սKf�2�����H�:%E����V�4̒d%�!�K�h�/�'Y3�OWjJA�C۱n̟(�J�Z��j�0�x���ɲ�(Ok�f�l�X�_�_;-jUZ"�LI���ؿ�:��1�g83\�
.��V��fu}:{�����j��'�=�X�
+v���`��Tr�)SJ���%[�1��	J_#z�u(��l�\��:�a}��1�����
��k�8�\�f�f`�Y,0{
{�ڃc�7����P��B$ �
+��������=�"�(Xl^�6��6L����u�5(x���W�����	ּ��t9���j�Ed��<��(7�2�Y^n�J���"���/�p�E�H@��x��z�u0�%P$$x�a��(�x�^��{��Vp��A�H��7s9U�B%<lZc���w"^1���A���ϒ���	��ql�}U&�#הB��OỖ������\�Br�KI�p�X`�Ȏ���NJBZ7��"�f��P]^!�5�������8A'�B;ϙ�����U�-�Hm(^^�H[x�~\�]��
+�-Γ�캖��zx|�����m�:RQ�qp>5NG��^������c���Im�V�����h���WL�Sko8��o��‚�R"���s��aK�:�sn�3���+�k���."h6�r�W�6��V��;^����u�!m�F?�K9�fqPF�++	,�M1���ͨ��*\q��]%)�T����p�Xaz�3Xo��<�4
�rZ������FިT
��짧��O��L�I����.n�v�C����yaZ5�kTL��w�@��s���{R���[� rA�ak����90���,�1_`��خ+_R3��_�pQU��?����>�AHruM���]Ӧ�E�@U���%&`=�.^d�jQ0$
<�c�*�
+̓n�sQT��ԧy���b�N���L���N9A���H�~ܕ}!E�H�iQIi�ps���,ΟM����LG���1}�R��_	/�����?V?t��R=��L�L�V������:�Ρʙ"������
+:����ᲄλJ(�f�D�t�klMB)]`r�	��!�䗏$�c�^�"8X������E���	�������^����sg�y��q́aiD��e��H��e~�#��M��vm!is���i�		]���Q��4��no/*;�`֘^C~�E�])���,�ږ�B�4�u֟��~^�ɰ�=o�1}�ClQF�Ϡ�1�X������b�ѷ��N���A�Xa�:�ǦZ��1�I�=�F1�\����X�fZ��]�A^iZjClQ�*��8%I�fBe��)E�QaŠ����a8DLr��#f��ec{kS€u`��[�ySU]K���C��Rj^~��in&�w���dgmM;�5�
+k۠��'�`4,��m�v
��E���!��b��Ů�JT�L	3w9Ss�A�.C��%���X����V�������~E�p��SkX��v-sU���8����E�~����uR{O�!��EX@��S=���~�-���o�1�dqX�RP��!7����S�SN�J�����
pdk�R���b��`�1R�	T�X;��5KCg���]~4�\�PD����!l+m6�D��6vUR.�y���/�“E��Q�4z�h�`M��x��.6]�M�JɃn�A�0o}kiT[��b-4��b+��i�^�1_^O
=m��$��R��%��.���K,?�̸P��%X]t��Kt�QGV����\��5ʚ<�>�
-k�}&���5�c�$����q���*���ne�=��_�����@2N&���|&�v0f�PB!���+]��l�%�>�.���KT�H�!���+f�9�${ub$!�#E�mςr��_��\�~�$e祕��)�۴�f����
+�m�0{�Q�d6��!j���Oa	�,�E}�{#��A��nhI��ž��x���L�ޅ�V�U�	7��Ԩ�iv�-Z��+�8튷��%��$:�3��T)�=��/A5�s�!
+d��?������FX~���g��5��ݡ+YR�Z����g|Z;#���f��U���M���u��/Wl0A-�ƷaKV)�|��Qw���
+z�Kbl���}�{�a
u1��OO��/I�~�1	sЄ:�5�S@F(�8(��Y�d�����/%("V"�$�l������u��nK�����BR�>]����]x[d�hj��5B6>�i�D��D)6ǝ���p�?�X�~^�^�M��R&�C-ܞ��Qb���ia�6�6͂�=�^���J<�x��`+ʆM�����6�6�sI��ɂ�FQOx�<$�D�ߡK�!2�*n!
�Al� �h�'�����Ї����>�\gJ����v�F)�c<����၍�e����(�R:8=�H��Q��"[횂w�z�f$�2�����ʭG�r���b���5t:`˙�Th�'�?�F�T9xm\JYTS��?�[�Mѹn�n�)�Ǜ����I�I��)�'n`�wI[���=�,.#Y�X[6�i�)3�4Q��s V'�>Ȗ����^(����K��B�qs�$�E�Z����5&II\�`X����v����F6���p@��E�p;��	 whQ����6����{r�}J
+)����8y�5�9���$��@��ȳ�qw=<��O��+k�m��'H�^�J`%LK葂�1����Jn�x�]R9\\�&[��)44v�#z���k�@���.�DEzA��W��[%A�+}MdGBâ�d��Έ�k�"�\����n�����خ~��#���x�Tj�Ւ��6՚s-+�p��f�<�:�ˍ_���jݑbĦ|6UGx(H�4�(��末��/����u,4^^��JT�oJ4v��w�Uz=Ӷ�Z�x�~`{��hF�Z�3'���������|�Ϧ�b�z�m��������I��*$�\z�� �xO!w�n�/���u]]���W ��ƫb�k����
+ݿ?>��q�4^x�yA�����h���B&�����~�E�B��AHZ���i_�:�`����-�����uM���s����D6%�A�!b�^�S[M��(]�B"�ԭ&��c����������sK��h�7�P��o�uL�Pq&������.�Q+�ͭ�EE�����P�A������=�l����/��H��ŭ�[W�)�~�g�;%��xC���a��Z��\�V���;�bO�i=�7~'����N�wb9
+˼@�h��i��,0�Yj���Y�.��R~�J��$�:Yu�S>V]�,
+��}[�A4W]�U�����m�{���[��И�H�!�v�!�����@����h�z�3Ӧ�X�*��rA,B���K��������:jV“;�)\��5����P��4���b�U��U�;�8�V��3@E�n��5G-ا�7+���x�чͱ`��d4��~كaq)�gj(gj�1��+�<�PsQC翵tQ5S) j�	j��%j�ڻ0�[IbDU��Q5�F�U��<:���P(2��u��U|�i�lpG�X�:U�T0jA-̅�{���s-���i?-�ю�%���6�Vj��0 ���a@4�)eƦ�
gla(g��T��3��C*�PV�ya(e��2���z�PDn@`W�dW��,k��/�����ٗ�Z��}Y�Tk���~�=��k�}t^1�*�<����
+��	)�܀��\R�5�-S�!b�u@O��v��˘m�ґ��|��G5'��
+�E�οϽVx�~�ÜT�&����*Hv�2��8�����2�{��vn���VF��L����%ɨ��LrQ0s1��-5�G���/�Σ���f��I.rf3.a�_���ބ"y�H�Q.��[��*,�X.fP�r؄]�/bs*-���{Oo]�ǔ�G�"�Xl܀P�_��k����7�J`��ӹV��&'���t/.��C�w��(��=�������HCrY��P�
��S��dFCr:��]q�@=H�LJ;�jH�rѐ|�} e�$��%�N,ߤV�����@
p�����|�$oq=#�jʒ�0��}	����ڥ�e��r����T��O�N�����P�"���t��8�!{�n]����\˼5SǞd���t~�pby��o��,�t����W%�N���:���6�B���*PDd2�cjk��K�~k��LR�4���5濵���ֆ0aK�
+��Z���rl�H5M�)��όC��ּ
++WC�Wy�L5��Ü�����!:��L��/���Ek��|���4@k"E�ju��Np�$Ĕ$�7  d��wճ�Tų)�S7�5�A%i��{D��F�`/�9��u��y�$�B=�1f4��iDI��;q�gr�8�O�%R^ގ��̙�� !n��@<�/ �ru �>\y;��>� ��D�IΙ�+gK%�G҅�K؀�w,ˇ~�-a��axm�y����f&\�湥ϗ�M�2�s���$���9?L�;�*)������v��܀��=���Mjs���@R�ܜ����L�	Cjs�=��s�9�.9�ϯR��#��οcs�����}��������ꈞ$�G��w��L��O�ư�����0&��%�ݽ���!XRj��u5Le$�j��Bm�5�8ddd�������t&���&3�C|�:��uX�f�2�
G��z��^����U����ja�zZG��6��P��K�
+����2��bק1 ��?��vr&��� ������5�|�����j�01��-������!�ď��84얓��ݳ�����D�i�cla(#�K�A�ē>���q��!��<dz=��m����i��K�)��c܀�
쉸�l<�*~�PcǔHT��hQ5V�I�@ޘM�1{c��썱�C	b6H�ă � 	4�~C�t����eR��kI�瑼�+��ø\ՙ���8�53 f{^��[i��w1�Tc���3�R�2��'�m,��O�Ռ��h��܀@���&͓����ˣ
+���$?�jS��r�f����~�ѫ����¸�Nɗ�nr��%NAñY��9���P����D�%����܀��-��ʴvOΐ<!)�|��ggD)�ש�Ԟ�e���C�x�!�A͸�4Q�[4�ʩMO\\������\|m��.yY`>�܀@8�&���ο�Ѵ���q�
+�v�!�Sb~�5�O(J��-�kkIfT]��I�l�~�җ�$�'�b�2��6�f��� )�Yɇ#��%-���pb��r������u��=�L w��=�=�=S#s�=���I9��y�x�j2���2� �`�,�����j@����L{���.\<_��LZn�.�e0d�.
��e�KV����mZ��l�E��%�%[�b�3��z�PC�	�Y���r����a0����⩸����<U�<Gnf�+ ����u��S�pv�-�_�ؕ����"K0R�]I�
܀�
܀�
$������y=���xa�������U�a���}U�I����3-&�.�X���j&8�C���o�!�R��4��\�z�Y2�o��@�F�}� 2m3���3���lvk�+�$��_���$ciD܀`Bi�~�.��jI2��Ѽk�ܿS.���Fk���^1b��Aּ�#i���Z��Z�,2*zkTp��LJd�Lq4�"��TI�@�jO4z��d$�u¿99~R�7�-~R������ؙߤG����|?I��A��<7���FG�?pŽ�$�#q����v�G1�]um�ya(K�'99*�
+���[ܲE�IVvQRĤ1>*G���̋���3�1T���ӳ���!�1Юl�����<�*%4�*�q.vF�&�$�B`�\�	(##��$���u`t	M�ەx��{Փ�a(�‰�J�g���
+&U�O�'�:�n@0K���
+s({�������7�|�r,�߲~�7"�o�\����q�I`�''_�L��A�E��+:�F4Ro����W��ο������P*�x�S�܈iV��s��5�(0�:܀�R�J�l��N��q���%��(��E�����%�4"��{�o��7�/[�B�4V�� �����H�$�IE33V�<0(PHV&: 0#�`$1�0�0s�1�n���~�jJ�pW:E��
+�}�6�[�O���XkpF2=~�$����_�qA]��8���"�c_�٦��;jz�MVz�{y��d�lg�%���,93E
���[��͎��Qǜs����L�5��C�0S���q���[�K�ye���㒋FD1��uH@��Lc��������4�z�H�w�HQ�h��P��[L*�s� �E��_��Jr���܉/���\}�x��
�f��u�MާO�ƛt���:����k���K.��ѵ�+|��w�y^~�խ�)tw-�
@��⢢K��W�� E�d�Mo�R��!0J7��zQ��F���\��пj.��">t ��Dž)��	�T:����
crQ���2�t~a�߿T@}@��F�3�STHW�˰��§�c��(����R͡�������T�*S�a�<m��=J�	꾟n���CG,K��A,=F5�t`��n9�ә
Pn��n΀<m-��)S�~���Sˢ+N������X�*��t|` �uFz2OsL,�N��7j$I�a�W�t�j/���|+W��m=��Rgv6�E��
�5���r㤓#$�!����P&H�ς��Y(V%�r%��+D��|!y8��-�b�Dj�dG��δէ��_1��z�Z<���J�(��C�:���Q����s��X�F�V��uD֛&�ɢ�G82(�n�3�������n_4��!J����Uf>�v�練�P��m��4�b����9c�KO����8�"6����;f��5�Q|�e)
+ �h��'����	�B����L�Y�����,������P�<h��,Y#
+S�!~��R>�V=�1�hՐ��AJ��\hb�A��=R<�'��F�`�&�g�u�'�y��kXN��0Q���7@�B�!<թ1Ρ�a��$dZD���Q*��(��6#T>���"�@�����j���W�/E .���m��,WV�y�D���_*���X����`���G��9Jо��lg���񰸊Wh3d+6’&����<Ll���k���M	�J�	if�NF�!��}�`�l,�8j��c�1f�@ ��v�r�W�gG�!�X�Vj0+�&5�h�_`�%�9��F��b�@h�O^�J�)l��%�����$PX��b~Tsֵ8������^Wz!�Y���b���?��!���F��$��^�iwǤ؛xxϠ���o�~��Xh>"3���ݚ�:'�;38�#�y�w��zX�}�(���n.��_�&e���
���D��f�5��!��?�W��]���gx��]#�Y.iU��Ew7�!�ߨ���y�d~kr6��?A�ţ��z`�O� Mc���4���e�B��n0��B�yS��ALP}��C��10NK���M�-5�FLN��bN�Z/ �f��8C��l�&�QXp���� �@�1 �����q�r�?�a��#��0����%C?m*������[)!�T*¼��5�S*��rʵv�85kX�>����a*/��R�_Hu��)p1�C���
+ G���8��v�(l�9�]P�Z���
��ºuƌ�s}��<�8���zMe������*?���M`Ɉ��CNY,)������2�,V�hK���/�Fy�.byP���^`Ċl�{��(z���Ŋl��~�Em(�R�r.�H���{��D7c��������X�jD>J���b��ՇX�!05���'�ѓ��$���C]�ൄz��sl���$T���Zmr�G�r۫�#0����'����]��.��I�!,��I>}�i~���������M�)�V���OՉ��HQO5֣��F��9�N���E� �̠Y_}-��.;���Lsn��J�ᘦ���?N��Œ��sޑԖ��"���At���Xy�D��M���p�����-�4�\�h}m`H�A{�_);�O���R��0�D�毄�s4=�f���։�\�К�`�I^J�J���i�=���uo����a�SL�2��Hς13m~ʋ��03�|v1�B'���P�J���������؎�R�;���1������{9[����l�F?�]KO�w�GM�#��m�қYֽ�
+���Y��
+�r\`-��+.�aM���Nv���@��$�q�@C��蜘֡G���ϲ��f��)�!KĩC�^��1ژ��jוO�zn��%�R�4���q�A�~�Z��/Ə��S�c?v�(������\;K7B�oW��f!��_X<������L��|"z5��UA��|�e�ڈT�G~&�f��_��tBt�H@��a�L���zf���9�fVɿ7���Df;�����}W>A��
+��T7��"
S��v�B�=���K�2�J���$��La5��G@jӨL�,%�� �A�#� �&`@ϛݺ��h�h�u]ւ��v�B7�W�_�CC�eO䖵`Z�~���.�hg��Rq9�(mS�!a]��u,)��"h��Ma�Գ̌�=��l���%M%?iVֱb�o�8̴���2�UE����oTGo*b�W���S퍥���ғ��)	x&}�(�A���Z����ė+�aߊGV.W��|zy$��H� z?Y�?A�K���Jbc�&��y@s�0�%��,J�']x�PR
+�^�6�4��5���c�c��@O2�v�����]����*�
p��j�fn����%�������8�3
�EЦ��i<5b=�Q�6���ޅ��s��;)m��@�jV�Oˬ!v�Q+�%��,E�eC8��nQ"&H�(�h'ev�Fe5�����yά�&Z;��p���~��O��	A�%�o�OÉRۉ��A��i�z��A�a]jT@^���p�Q
+��;�
+������\"�޼�yb߬CLk���1 �
+��wʜh��0c�W���]��d(�!����,Pi�W�x@�kf��['�a!v/�:��z��B���ӣ�>�|�Ô����tu<�
8��[؞v��͡�[l7����Q�h<�ov)=\X3�9��,�L_7$d����A�Bzx�{hV�)=L"䈵���m=ɝ;s�T�<�\ڒ�,�Q��
+�j��H�=1zx��!�c^�8{z��u|�=����y��D~
6�N��ɏ{d���ۈ�xX���J!���ze��Ѻ���=��>�Ru,�Fi
+,��aBB�M\�RO�AE�t]zX���Mu@�IT�#ȴ�����A�zFG[��qXxl�������nB��C2�>���y��0�������k�q�6f�[�`��;�$�{
+�iq�
+�,�T3�==١�^џ+�N2l.ʫp����܊&1��AraE �#��;����}��#j��@w�"�F�#*���J�e	�1�Uzؔm$���=��O��r5R?د9���ζ���e�����T�J^T�T2H�נ6P���'D���I8
7{
+q?�[F��#�B�a����MNH�қk(�!fz������SH�l/���
?>�C��7���h�T?�����*�d:�=�%���B{m�{��mv�b@��m�0r�jB�AM&�x�8!���l�x�B!�5=lm~��#����ìd+�����������DG
n���S�����ƚ�a0|%9�.��{�ok
+qَ�!���=B�с��˦����k���gAAB/~���@����0��0��\�����T胧+��4`��Yb���x-q1EizT\�'M�L%SD��إ��F���Q
�"\���EyK|�7\��A�B���X�,L�P
�8�ӌ���3Ac9�;�L���������&��c
+�aIP�]'�S��4��ɰ�OR�zq�m�ﷇS�*$��VТ��:��}�Y��|�K�W����Ԡ���3�´}q���G��:e��+σ��`C|F��k��)����%�*�bJň(:�<�ߙ|�e�޺Q������)=<�&c��D��.�*�ʩ����� ��+�N<�+}��r��d��bq�5E
+�OP q���<>��*�!�#C��\|�!��2�[������y?�0��E1�"�\��O93��5����A��O�T��O3?�*�ᘟ�����S�+c�l|����QKX)��I ��L�*���)�ϝ2Q�x՘|ص����^��5
+��;i�N�.����ځI�D|E��j(O ��'q��&�x�IM�"��z�����2��䵕}��bаmN�����
�SUaY�ѥ�p��C�ύA'n͆��O�0�x�Utr�� :DCzY�8�P���L��!�٧4�P��S{/(�闔�t,UZ�,}l�C}��X�*���Ê�a�Y�����*-��w��׫��U�X[0���Ԝ!_��|�t$�?#�����cC"*E[������|Ј �ǥ��I�|C6O�����a�����kEz��ݮ���!1�٫$��Ievl�A:�Q���g���/skب���MB��yIkQ��
�=�-m(��pb|��>_���:B:�}ۻ����h�ɔ[�D��QR!��
{Y@�gDp��>e��8봯�<#�� |��vȷ&�E���<�d�sPo��g0��<�r_b�r�#�mF�A/nf�k��8�5�l����aT���Ayd�3���\�R����^�W2��
+�(���a,�M���K��'���S�{��1��zi7Y-h�'ڦ�&��<�<�2~�9�����?�ɷ�7M��{�"����6�3z�z'X�M��ڶ���(="������o:�����4��|B�D��+R���*��U
+�y�Cd�5��Ì�6)x���d���z���Q��v����څ�����~1Y��`]�!=�G�v��Nr���w�t<أK��5��B�_�:,�(M�z�<a�_���4��h���9�a��]�&e�Ք�n�F������I'�q��J(�	��_D�+��zh��,(��G$wt�kg�0���Np+<v�	�N��$�H��_�a����m��I�0�n��Z����q�����jLH�� ����)����R�����Gu�j7�r����0U4H����-xL	L0�V� 觸�j0�>9E�$�0����(WP׶�m��yeO� a٭0�<���=^NϙP������U;˪ƹ
+�z%��"����-)�XFT��al�y���*�/)r�#Px�c\ѬFZ�E���٢���#��OwFX1��!��r�D���,9��
3e
+�9
0+`��Q�v̇U��NB2Dȕ�e`�ڶ�-��?0n}
+�Qk�����H�0���E������<�D�2�ҳ��Z�+����|��
�/�5����L��p�
+�3�US�#Ö����_"d.z� ���N�۔z�u�Q�$LΥx�s�%]2��(ѝm�A8f�kv�����!��6����1�Xՠj�HZФ���~N�鑊������%�Al��1�sI�_����fK�cqu�Ԡ<0E�	�"_���h�
+�1t�j�B�U�Ӡ���(�(F� �_oQ�T5W�D�}��BL]ܹo��&����(q�os�'��#�i'*��ehܗ��^)�����侥������W6�%3�l�j^�����;CY﹯��T�ŏ?�X揄㾢�Zh���A�-���X=e�LK�n�(H���N}yoa��.͔�HL�My�}�N#�xd��I(s�n�$�y�~�i'�ꕬF�������TUQ	���wLN��{ ��Ȧ�����g�}Y�?�
�!)Z�G)ڛ}���@�&k���L�ҥ*�׹�87#�r����HQ�Ͼ�ǃR?�Kw#��S�Ps_��`x�t��\u#�ND���sߔ
)Q�-��,�(�+�ᝫ5�+zj��N��5Ũ-���}�E,�"9r q�q��������*@	���N_��#öӚ�J�ao!�֓Z`��Ӈ����%<
3j8p�*�����?�;�[��wjS�������"�����r��g����k t���{ED���݊���Hel+֡7��h���e@���!CQ�t�-�M��4��n��-�w��	�%x7��4Ձ��N.���˺}�Wh�5!XYW�o�C,�����a���	l��	Ȅ�Z��<�L��e�r*|������-�r����
+�U�f.ڝ�W��0�[���G���b.=c�N�R�[$�u9��]ӖE�>�y��=	���&���ȑ�$�Mhdȭ%�ln��@]����µxX��ԕ%3��3�	H�T����j�n
u`8���{`(#rd|�2�O�A��,�1�{���຅��z5t��(���ځU���F�c^�)�;I`�4���2����=�|���wch���Τ<W~@p�'���w<���Um��snc޾nPbn�"��4���b�n^9W�{w݋��0���shv�m2\NR�t���]�/�&�.��l`MMD��jjwj�֙([Q�Y��1������T�2I�E$�e�\���"]S|�@��$���cU��E0
�؂��$j�0w ��ow���t��2�!�{:F�چ�@���h�I
<-�X�'/�m�K���d���m*!kk2�W	XӋd�����(K	b2�8M�QZxO�)�)�PLNИׯ�Be�)H��ER�N�ǙB�M�iwU��j�؋Dž$��ӏ5ӥNq����m��t<��|�+�������~$9��>g#���J��Ȍ��3�8���΋i:��`F�'�Ϋ�{��`�O1�w���w^�%���D�λ�C|��otIЫ�0\�y���q=��.��yˀX�mIsY�d�V��m@t���a�S"���n����_���������R�N�r��+���{�K�dzBV����V�8Ȑ�!3#a�М4���:7 ��nqG'x���$޾�J�Ԁ��Q��ޡ�9������<.��F"��UY��Q��	��m S��Y0p��Ɯb��[��q0oM���B��������&�;E�fw�������C_0��qp����
��T��C���	��P�XnE��[:r�k�HG��IFΙRU 8L�B�YO �z�d�ܜ���6��Č}��
�����
+���5�����F��u��O�F����Z.�>�aB���)Ů���g@Xs~RrY@�U�0���8��xŖ?R�9�������L�YGl�x�Uԏ�N�C"󢕾�x�F��Ѡ9+^�.#H``k�胃�J���-���;�[F��H!+FM@	�â	%	:_�c:b!OX��Tz=)�X�Y!��2j�@��O��	��;����*��6:0^d��0g�[bq���;i�T�M)���N���<J������&�\�|�Tr��<����O��,�W�9�f)�6�c�̡���|�N�,aQW4#��䎋�[-�J��pz��X�=X�
+.w�]bt�H%c���3����i6���#�yPi�$�]f��p���=���?6];�j֬����w��-q�՚��.�|��J��AVZZ��Y�:NLv�2<nz��"�q�V@*��h?y�W���-pI�nYGN��&��K��L�q�����.QԻ.�|�|��+E�T��؜s�a��'�#�|S���${I��
+endstream
endobj
23 0 obj
[22 0 R]
endobj
33 0 obj
<</CreationDate(D:20200503203529-04'00')/Creator(Adobe Illustrator 24.1 \(Windows\))/ModDate(D:20200503203530-04'00')/Producer(Adobe PDF library 15.00)/Title(bg)>>
endobj
xref
+0 34
+0000000004 65535 f
+0000000016 00000 n
+0000000147 00000 n
+0000036803 00000 n
+0000000000 00000 f
+0000036854 00000 n
+0000000000 00000 f
+0000041104 00000 n
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000041177 00000 n
+0000041373 00000 n
+0000042509 00000 n
+0000108098 00000 n
+0000173687 00000 n
+0000000000 00000 f
+0000038120 00000 n
+0000202541 00000 n
+0000037232 00000 n
+0000038420 00000 n
+0000038307 00000 n
+0000037396 00000 n
+0000037558 00000 n
+0000037606 00000 n
+0000038191 00000 n
+0000038222 00000 n
+0000038455 00000 n
+0000202566 00000 n
+trailer
+<</Size 34/Root 1 0 R/Info 33 0 R/ID[<BC977FA991A5D94098E95560BB7F12C6><D1F48F1DE6FC024B90841EA9B5AFCEC1>]>>
+startxref
+202746
+%%EOF
diff --git a/system/javascript/osapjs/client/interface/bg.png b/system/javascript/osapjs/client/interface/bg.png
new file mode 100644
index 0000000000000000000000000000000000000000..b77eee81f9212a98d54206095f0cf00a5bb24f66
Binary files /dev/null and b/system/javascript/osapjs/client/interface/bg.png differ
diff --git a/system/javascript/osapjs/client/interface/domTools.js b/system/javascript/osapjs/client/interface/domTools.js
new file mode 100644
index 0000000000000000000000000000000000000000..2bd34a3c32f49afe427d19f781a57a002131ce67
--- /dev/null
+++ b/system/javascript/osapjs/client/interface/domTools.js
@@ -0,0 +1,105 @@
+/*
+domtools.js
+
+osap tool drawing utility
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// -------------------------------------------------------- TRANSFORM
+
+// move things,
+let writeTransform = (div, tf) => {
+  //console.log('vname, div, tf', view.name, div, tf)
+  if (tf.s) {
+    div.style.transform = `scale(${tf.s})`
+  } else {
+    div.style.transform = `scale(1)`
+  }
+  //div.style.transformOrigin = `${tf.ox}px ${tf.oy}px`
+  div.style.left = `${parseInt(tf.x)}px`
+  div.style.top = `${parseInt(tf.y)}px`
+}
+
+// a utility to do the same, for the background, for *the illusion of movement*,
+// as a note: something is wrongo with this, background doth not zoom at the same rate...
+let writeBackgroundTransform = (div, tf) => {
+  div.style.backgroundSize = `${tf.s * 10}px ${tf.s * 10}px`
+  div.style.backgroundPosition = `${tf.x + 50*(1-tf.s)}px ${tf.y + 50*(1-tf.s)}px`
+}
+
+// a uility to read those transforms out of elements,
+// herein lays ancient mods code, probably a better implementation exists
+let readTransform = (div) => {
+  // transform, for scale
+  let transform = div.style.transform
+  let index = transform.indexOf('scale')
+  let left = transform.indexOf('(', index)
+  let right = transform.indexOf(')', index)
+  let s = parseFloat(transform.slice(left + 1, right))
+
+  // left and right, position
+  let x = parseFloat(div.style.left)
+  let y = parseFloat(div.style.top)
+
+  return ({
+    s: s,
+    x: x,
+    y: y
+  })
+}
+
+let placeField = (field, width, height, xpos, ypos) => {
+  $(field).css('position', 'absolute')
+      .css('border', 'none')
+      .css('width', `${width}px`)
+      .css('height', `${height}px`)
+  $($('.plane').get(0)).append(field)
+  let dft = { s: 1, x: xpos, y: ypos, ox: 0, oy: 0 }
+  writeTransform(field, dft)
+}
+
+// -------------------------------------------------------- DRAG Attach / Detach Utility
+
+let dragTool = (dragHandler, upHandler) => {
+  let onUp = (evt) => {
+    if (upHandler) upHandler(evt)
+    window.removeEventListener('mousemove', dragHandler)
+    window.removeEventListener('mouseup', onUp)
+  }
+  window.addEventListener('mousemove', dragHandler)
+  window.addEventListener('mouseup', onUp)
+}
+
+// -------------------------------------------------------- SVG
+
+// return in an absolute-positioned wrapper at ax, ay, with dx / dy endpoint
+let svgLine = (ax, ay, dx, dy, width = 1, id = "svgLine") => {
+  let cont = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
+  $(cont).addClass('svgcont').attr('id', id).css('left', ax).css('top', ay)
+  let path = document.createElementNS('http://www.w3.org/2000/svg', 'line')
+  $(cont).append(path)
+  path.style.stroke = '#1a1a1a'
+  path.style.fill = 'none'
+  path.style.strokeWidth = `${width}px`
+  path.setAttribute('x1', 0)
+  path.setAttribute('y1', 0)
+  path.setAttribute('x2', dx)
+  path.setAttribute('y2', dy)
+  return cont
+}
+
+export default {
+  placeField,
+  writeTransform,
+  writeBackgroundTransform,
+  readTransform,
+  dragTool,
+  svgLine
+}
diff --git a/system/javascript/osapjs/client/interface/grid.js b/system/javascript/osapjs/client/interface/grid.js
new file mode 100644
index 0000000000000000000000000000000000000000..f204a3a806a3b662b4bf392eda06c6d1cff1f6f3
--- /dev/null
+++ b/system/javascript/osapjs/client/interface/grid.js
@@ -0,0 +1,78 @@
+/*
+grid.js
+
+... drawing tools 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import DT from './domTools.js'
+
+export default function Grid(){
+    // ------------------------------------------------------ PLANE / ZOOM / PAN
+  let plane = $('<div>').addClass('plane').get(0)
+  let wrapper = $('#wrapper').get(0)
+  // odd, but w/o this, scaling the plane & the background together introduces some numerical errs,
+  // probably because the DOM is scaling a zero-size plane, or somesuch.
+  $(plane).css('background', 'url("/osapjs/client/interface/bg.png")').css('width', '100px').css('height', '100px')
+  let cs = 1 // current scale,
+  let dft = { s: cs, x: 0, y: 0, ox: 0, oy: 0 } // default transform
+
+  // zoom on wheel
+  wrapper.addEventListener('wheel', (evt) => {
+    if ($(evt.target).is('input, textarea')) return
+    evt.preventDefault()
+    evt.stopPropagation()
+
+    let ox = evt.clientX
+    let oy = evt.clientY
+
+    let ds
+    if (evt.deltaY > 0) {
+      ds = 0.025
+    } else {
+      ds = -0.025
+    }
+
+    let ct = DT.readTransform(plane)
+    ct.s *= 1 + ds
+    ct.x += (ct.x - ox) * ds
+    ct.y += (ct.y - oy) * ds
+
+    // max zoom pls thx
+    if (ct.s > 1.5) ct.s = 1.5
+    if (ct.s < 0.05) ct.s = 0.05
+    cs = ct.s
+    DT.writeTransform(plane, ct)
+    DT.writeBackgroundTransform(wrapper, ct)
+  })
+
+  // pan on drag,
+  wrapper.addEventListener('mousedown', (evt) => {
+    //console.log(evt.target, $(evt.target).is('svg'))
+    if (!($(evt.target).is('#wrapper'))) return; // && !($(evt.target).is('svg'))) return
+    evt.preventDefault()
+    evt.stopPropagation()
+    DT.dragTool((drag) => {
+      drag.preventDefault()
+      drag.stopPropagation()
+      let ct = DT.readTransform(plane)
+      ct.x += drag.movementX
+      ct.y += drag.movementY
+      DT.writeTransform(plane, ct)
+      DT.writeBackgroundTransform(wrapper, ct)
+    })
+  })
+
+  // init w/ defaults,
+  DT.writeTransform(plane, dft)
+  DT.writeBackgroundTransform(wrapper, dft)
+
+  $(wrapper).append(plane)
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/interface/style.js b/system/javascript/osapjs/client/interface/style.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9e8800854af80cad469187ca7c995ff33d68609
--- /dev/null
+++ b/system/javascript/osapjs/client/interface/style.js
@@ -0,0 +1,8 @@
+// haha, derp
+
+export default {
+    red: 'rgb(242, 201, 201)',
+    grn: 'rgb(201, 242, 201)',
+    ylw: 'rgb(240, 240, 180)',
+    grey: 'rgb(242, 242, 242)',
+}
diff --git a/system/javascript/osapjs/client/libs/.gitignore b/system/javascript/osapjs/client/libs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/system/javascript/osapjs/client/libs/d3.js b/system/javascript/osapjs/client/libs/d3.js
new file mode 100644
index 0000000000000000000000000000000000000000..b11658f9f2c1bf68d7b7b0eb35bc1d48b6f0fc8a
--- /dev/null
+++ b/system/javascript/osapjs/client/libs/d3.js
@@ -0,0 +1,18293 @@
+// https://d3js.org v5.9.2 Copyright 2019 Mike Bostock
+(function (global, factory) {
+typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+typeof define === 'function' && define.amd ? define(['exports'], factory) :
+(factory((global.d3 = global.d3 || {})));
+}(this, (function (exports) { 'use strict';
+
+var version = "5.9.2";
+
+function ascending(a, b) {
+  return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+}
+
+function bisector(compare) {
+  if (compare.length === 1) compare = ascendingComparator(compare);
+  return {
+    left: function(a, x, lo, hi) {
+      if (lo == null) lo = 0;
+      if (hi == null) hi = a.length;
+      while (lo < hi) {
+        var mid = lo + hi >>> 1;
+        if (compare(a[mid], x) < 0) lo = mid + 1;
+        else hi = mid;
+      }
+      return lo;
+    },
+    right: function(a, x, lo, hi) {
+      if (lo == null) lo = 0;
+      if (hi == null) hi = a.length;
+      while (lo < hi) {
+        var mid = lo + hi >>> 1;
+        if (compare(a[mid], x) > 0) hi = mid;
+        else lo = mid + 1;
+      }
+      return lo;
+    }
+  };
+}
+
+function ascendingComparator(f) {
+  return function(d, x) {
+    return ascending(f(d), x);
+  };
+}
+
+var ascendingBisect = bisector(ascending);
+var bisectRight = ascendingBisect.right;
+var bisectLeft = ascendingBisect.left;
+
+function pairs(array, f) {
+  if (f == null) f = pair;
+  var i = 0, n = array.length - 1, p = array[0], pairs = new Array(n < 0 ? 0 : n);
+  while (i < n) pairs[i] = f(p, p = array[++i]);
+  return pairs;
+}
+
+function pair(a, b) {
+  return [a, b];
+}
+
+function cross(values0, values1, reduce) {
+  var n0 = values0.length,
+      n1 = values1.length,
+      values = new Array(n0 * n1),
+      i0,
+      i1,
+      i,
+      value0;
+
+  if (reduce == null) reduce = pair;
+
+  for (i0 = i = 0; i0 < n0; ++i0) {
+    for (value0 = values0[i0], i1 = 0; i1 < n1; ++i1, ++i) {
+      values[i] = reduce(value0, values1[i1]);
+    }
+  }
+
+  return values;
+}
+
+function descending(a, b) {
+  return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+}
+
+function number(x) {
+  return x === null ? NaN : +x;
+}
+
+function variance(values, valueof) {
+  var n = values.length,
+      m = 0,
+      i = -1,
+      mean = 0,
+      value,
+      delta,
+      sum = 0;
+
+  if (valueof == null) {
+    while (++i < n) {
+      if (!isNaN(value = number(values[i]))) {
+        delta = value - mean;
+        mean += delta / ++m;
+        sum += delta * (value - mean);
+      }
+    }
+  }
+
+  else {
+    while (++i < n) {
+      if (!isNaN(value = number(valueof(values[i], i, values)))) {
+        delta = value - mean;
+        mean += delta / ++m;
+        sum += delta * (value - mean);
+      }
+    }
+  }
+
+  if (m > 1) return sum / (m - 1);
+}
+
+function deviation(array, f) {
+  var v = variance(array, f);
+  return v ? Math.sqrt(v) : v;
+}
+
+function extent(values, valueof) {
+  var n = values.length,
+      i = -1,
+      value,
+      min,
+      max;
+
+  if (valueof == null) {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = values[i]) != null && value >= value) {
+        min = max = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = values[i]) != null) {
+            if (min > value) min = value;
+            if (max < value) max = value;
+          }
+        }
+      }
+    }
+  }
+
+  else {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = valueof(values[i], i, values)) != null && value >= value) {
+        min = max = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = valueof(values[i], i, values)) != null) {
+            if (min > value) min = value;
+            if (max < value) max = value;
+          }
+        }
+      }
+    }
+  }
+
+  return [min, max];
+}
+
+var array = Array.prototype;
+
+var slice = array.slice;
+var map = array.map;
+
+function constant(x) {
+  return function() {
+    return x;
+  };
+}
+
+function identity(x) {
+  return x;
+}
+
+function sequence(start, stop, step) {
+  start = +start, stop = +stop, step = (n = arguments.length) < 2 ? (stop = start, start = 0, 1) : n < 3 ? 1 : +step;
+
+  var i = -1,
+      n = Math.max(0, Math.ceil((stop - start) / step)) | 0,
+      range = new Array(n);
+
+  while (++i < n) {
+    range[i] = start + i * step;
+  }
+
+  return range;
+}
+
+var e10 = Math.sqrt(50),
+    e5 = Math.sqrt(10),
+    e2 = Math.sqrt(2);
+
+function ticks(start, stop, count) {
+  var reverse,
+      i = -1,
+      n,
+      ticks,
+      step;
+
+  stop = +stop, start = +start, count = +count;
+  if (start === stop && count > 0) return [start];
+  if (reverse = stop < start) n = start, start = stop, stop = n;
+  if ((step = tickIncrement(start, stop, count)) === 0 || !isFinite(step)) return [];
+
+  if (step > 0) {
+    start = Math.ceil(start / step);
+    stop = Math.floor(stop / step);
+    ticks = new Array(n = Math.ceil(stop - start + 1));
+    while (++i < n) ticks[i] = (start + i) * step;
+  } else {
+    start = Math.floor(start * step);
+    stop = Math.ceil(stop * step);
+    ticks = new Array(n = Math.ceil(start - stop + 1));
+    while (++i < n) ticks[i] = (start - i) / step;
+  }
+
+  if (reverse) ticks.reverse();
+
+  return ticks;
+}
+
+function tickIncrement(start, stop, count) {
+  var step = (stop - start) / Math.max(0, count),
+      power = Math.floor(Math.log(step) / Math.LN10),
+      error = step / Math.pow(10, power);
+  return power >= 0
+      ? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(10, power)
+      : -Math.pow(10, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1);
+}
+
+function tickStep(start, stop, count) {
+  var step0 = Math.abs(stop - start) / Math.max(0, count),
+      step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
+      error = step0 / step1;
+  if (error >= e10) step1 *= 10;
+  else if (error >= e5) step1 *= 5;
+  else if (error >= e2) step1 *= 2;
+  return stop < start ? -step1 : step1;
+}
+
+function thresholdSturges(values) {
+  return Math.ceil(Math.log(values.length) / Math.LN2) + 1;
+}
+
+function histogram() {
+  var value = identity,
+      domain = extent,
+      threshold = thresholdSturges;
+
+  function histogram(data) {
+    var i,
+        n = data.length,
+        x,
+        values = new Array(n);
+
+    for (i = 0; i < n; ++i) {
+      values[i] = value(data[i], i, data);
+    }
+
+    var xz = domain(values),
+        x0 = xz[0],
+        x1 = xz[1],
+        tz = threshold(values, x0, x1);
+
+    // Convert number of thresholds into uniform thresholds.
+    if (!Array.isArray(tz)) {
+      tz = tickStep(x0, x1, tz);
+      tz = sequence(Math.ceil(x0 / tz) * tz, x1, tz); // exclusive
+    }
+
+    // Remove any thresholds outside the domain.
+    var m = tz.length;
+    while (tz[0] <= x0) tz.shift(), --m;
+    while (tz[m - 1] > x1) tz.pop(), --m;
+
+    var bins = new Array(m + 1),
+        bin;
+
+    // Initialize bins.
+    for (i = 0; i <= m; ++i) {
+      bin = bins[i] = [];
+      bin.x0 = i > 0 ? tz[i - 1] : x0;
+      bin.x1 = i < m ? tz[i] : x1;
+    }
+
+    // Assign data to bins by value, ignoring any outside the domain.
+    for (i = 0; i < n; ++i) {
+      x = values[i];
+      if (x0 <= x && x <= x1) {
+        bins[bisectRight(tz, x, 0, m)].push(data[i]);
+      }
+    }
+
+    return bins;
+  }
+
+  histogram.value = function(_) {
+    return arguments.length ? (value = typeof _ === "function" ? _ : constant(_), histogram) : value;
+  };
+
+  histogram.domain = function(_) {
+    return arguments.length ? (domain = typeof _ === "function" ? _ : constant([_[0], _[1]]), histogram) : domain;
+  };
+
+  histogram.thresholds = function(_) {
+    return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), histogram) : threshold;
+  };
+
+  return histogram;
+}
+
+function threshold(values, p, valueof) {
+  if (valueof == null) valueof = number;
+  if (!(n = values.length)) return;
+  if ((p = +p) <= 0 || n < 2) return +valueof(values[0], 0, values);
+  if (p >= 1) return +valueof(values[n - 1], n - 1, values);
+  var n,
+      i = (n - 1) * p,
+      i0 = Math.floor(i),
+      value0 = +valueof(values[i0], i0, values),
+      value1 = +valueof(values[i0 + 1], i0 + 1, values);
+  return value0 + (value1 - value0) * (i - i0);
+}
+
+function freedmanDiaconis(values, min, max) {
+  values = map.call(values, number).sort(ascending);
+  return Math.ceil((max - min) / (2 * (threshold(values, 0.75) - threshold(values, 0.25)) * Math.pow(values.length, -1 / 3)));
+}
+
+function scott(values, min, max) {
+  return Math.ceil((max - min) / (3.5 * deviation(values) * Math.pow(values.length, -1 / 3)));
+}
+
+function max(values, valueof) {
+  var n = values.length,
+      i = -1,
+      value,
+      max;
+
+  if (valueof == null) {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = values[i]) != null && value >= value) {
+        max = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = values[i]) != null && value > max) {
+            max = value;
+          }
+        }
+      }
+    }
+  }
+
+  else {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = valueof(values[i], i, values)) != null && value >= value) {
+        max = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = valueof(values[i], i, values)) != null && value > max) {
+            max = value;
+          }
+        }
+      }
+    }
+  }
+
+  return max;
+}
+
+function mean(values, valueof) {
+  var n = values.length,
+      m = n,
+      i = -1,
+      value,
+      sum = 0;
+
+  if (valueof == null) {
+    while (++i < n) {
+      if (!isNaN(value = number(values[i]))) sum += value;
+      else --m;
+    }
+  }
+
+  else {
+    while (++i < n) {
+      if (!isNaN(value = number(valueof(values[i], i, values)))) sum += value;
+      else --m;
+    }
+  }
+
+  if (m) return sum / m;
+}
+
+function median(values, valueof) {
+  var n = values.length,
+      i = -1,
+      value,
+      numbers = [];
+
+  if (valueof == null) {
+    while (++i < n) {
+      if (!isNaN(value = number(values[i]))) {
+        numbers.push(value);
+      }
+    }
+  }
+
+  else {
+    while (++i < n) {
+      if (!isNaN(value = number(valueof(values[i], i, values)))) {
+        numbers.push(value);
+      }
+    }
+  }
+
+  return threshold(numbers.sort(ascending), 0.5);
+}
+
+function merge(arrays) {
+  var n = arrays.length,
+      m,
+      i = -1,
+      j = 0,
+      merged,
+      array;
+
+  while (++i < n) j += arrays[i].length;
+  merged = new Array(j);
+
+  while (--n >= 0) {
+    array = arrays[n];
+    m = array.length;
+    while (--m >= 0) {
+      merged[--j] = array[m];
+    }
+  }
+
+  return merged;
+}
+
+function min(values, valueof) {
+  var n = values.length,
+      i = -1,
+      value,
+      min;
+
+  if (valueof == null) {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = values[i]) != null && value >= value) {
+        min = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = values[i]) != null && min > value) {
+            min = value;
+          }
+        }
+      }
+    }
+  }
+
+  else {
+    while (++i < n) { // Find the first comparable value.
+      if ((value = valueof(values[i], i, values)) != null && value >= value) {
+        min = value;
+        while (++i < n) { // Compare the remaining values.
+          if ((value = valueof(values[i], i, values)) != null && min > value) {
+            min = value;
+          }
+        }
+      }
+    }
+  }
+
+  return min;
+}
+
+function permute(array, indexes) {
+  var i = indexes.length, permutes = new Array(i);
+  while (i--) permutes[i] = array[indexes[i]];
+  return permutes;
+}
+
+function scan(values, compare) {
+  if (!(n = values.length)) return;
+  var n,
+      i = 0,
+      j = 0,
+      xi,
+      xj = values[j];
+
+  if (compare == null) compare = ascending;
+
+  while (++i < n) {
+    if (compare(xi = values[i], xj) < 0 || compare(xj, xj) !== 0) {
+      xj = xi, j = i;
+    }
+  }
+
+  if (compare(xj, xj) === 0) return j;
+}
+
+function shuffle(array, i0, i1) {
+  var m = (i1 == null ? array.length : i1) - (i0 = i0 == null ? 0 : +i0),
+      t,
+      i;
+
+  while (m) {
+    i = Math.random() * m-- | 0;
+    t = array[m + i0];
+    array[m + i0] = array[i + i0];
+    array[i + i0] = t;
+  }
+
+  return array;
+}
+
+function sum(values, valueof) {
+  var n = values.length,
+      i = -1,
+      value,
+      sum = 0;
+
+  if (valueof == null) {
+    while (++i < n) {
+      if (value = +values[i]) sum += value; // Note: zero and null are equivalent.
+    }
+  }
+
+  else {
+    while (++i < n) {
+      if (value = +valueof(values[i], i, values)) sum += value;
+    }
+  }
+
+  return sum;
+}
+
+function transpose(matrix) {
+  if (!(n = matrix.length)) return [];
+  for (var i = -1, m = min(matrix, length), transpose = new Array(m); ++i < m;) {
+    for (var j = -1, n, row = transpose[i] = new Array(n); ++j < n;) {
+      row[j] = matrix[j][i];
+    }
+  }
+  return transpose;
+}
+
+function length(d) {
+  return d.length;
+}
+
+function zip() {
+  return transpose(arguments);
+}
+
+var slice$1 = Array.prototype.slice;
+
+function identity$1(x) {
+  return x;
+}
+
+var top = 1,
+    right = 2,
+    bottom = 3,
+    left = 4,
+    epsilon = 1e-6;
+
+function translateX(x) {
+  return "translate(" + (x + 0.5) + ",0)";
+}
+
+function translateY(y) {
+  return "translate(0," + (y + 0.5) + ")";
+}
+
+function number$1(scale) {
+  return function(d) {
+    return +scale(d);
+  };
+}
+
+function center(scale) {
+  var offset = Math.max(0, scale.bandwidth() - 1) / 2; // Adjust for 0.5px offset.
+  if (scale.round()) offset = Math.round(offset);
+  return function(d) {
+    return +scale(d) + offset;
+  };
+}
+
+function entering() {
+  return !this.__axis;
+}
+
+function axis(orient, scale) {
+  var tickArguments = [],
+      tickValues = null,
+      tickFormat = null,
+      tickSizeInner = 6,
+      tickSizeOuter = 6,
+      tickPadding = 3,
+      k = orient === top || orient === left ? -1 : 1,
+      x = orient === left || orient === right ? "x" : "y",
+      transform = orient === top || orient === bottom ? translateX : translateY;
+
+  function axis(context) {
+    var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues,
+        format = tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity$1) : tickFormat,
+        spacing = Math.max(tickSizeInner, 0) + tickPadding,
+        range = scale.range(),
+        range0 = +range[0] + 0.5,
+        range1 = +range[range.length - 1] + 0.5,
+        position = (scale.bandwidth ? center : number$1)(scale.copy()),
+        selection = context.selection ? context.selection() : context,
+        path = selection.selectAll(".domain").data([null]),
+        tick = selection.selectAll(".tick").data(values, scale).order(),
+        tickExit = tick.exit(),
+        tickEnter = tick.enter().append("g").attr("class", "tick"),
+        line = tick.select("line"),
+        text = tick.select("text");
+
+    path = path.merge(path.enter().insert("path", ".tick")
+        .attr("class", "domain")
+        .attr("stroke", "currentColor"));
+
+    tick = tick.merge(tickEnter);
+
+    line = line.merge(tickEnter.append("line")
+        .attr("stroke", "currentColor")
+        .attr(x + "2", k * tickSizeInner));
+
+    text = text.merge(tickEnter.append("text")
+        .attr("fill", "currentColor")
+        .attr(x, k * spacing)
+        .attr("dy", orient === top ? "0em" : orient === bottom ? "0.71em" : "0.32em"));
+
+    if (context !== selection) {
+      path = path.transition(context);
+      tick = tick.transition(context);
+      line = line.transition(context);
+      text = text.transition(context);
+
+      tickExit = tickExit.transition(context)
+          .attr("opacity", epsilon)
+          .attr("transform", function(d) { return isFinite(d = position(d)) ? transform(d) : this.getAttribute("transform"); });
+
+      tickEnter
+          .attr("opacity", epsilon)
+          .attr("transform", function(d) { var p = this.parentNode.__axis; return transform(p && isFinite(p = p(d)) ? p : position(d)); });
+    }
+
+    tickExit.remove();
+
+    path
+        .attr("d", orient === left || orient == right
+            ? (tickSizeOuter ? "M" + k * tickSizeOuter + "," + range0 + "H0.5V" + range1 + "H" + k * tickSizeOuter : "M0.5," + range0 + "V" + range1)
+            : (tickSizeOuter ? "M" + range0 + "," + k * tickSizeOuter + "V0.5H" + range1 + "V" + k * tickSizeOuter : "M" + range0 + ",0.5H" + range1));
+
+    tick
+        .attr("opacity", 1)
+        .attr("transform", function(d) { return transform(position(d)); });
+
+    line
+        .attr(x + "2", k * tickSizeInner);
+
+    text
+        .attr(x, k * spacing)
+        .text(format);
+
+    selection.filter(entering)
+        .attr("fill", "none")
+        .attr("font-size", 10)
+        .attr("font-family", "sans-serif")
+        .attr("text-anchor", orient === right ? "start" : orient === left ? "end" : "middle");
+
+    selection
+        .each(function() { this.__axis = position; });
+  }
+
+  axis.scale = function(_) {
+    return arguments.length ? (scale = _, axis) : scale;
+  };
+
+  axis.ticks = function() {
+    return tickArguments = slice$1.call(arguments), axis;
+  };
+
+  axis.tickArguments = function(_) {
+    return arguments.length ? (tickArguments = _ == null ? [] : slice$1.call(_), axis) : tickArguments.slice();
+  };
+
+  axis.tickValues = function(_) {
+    return arguments.length ? (tickValues = _ == null ? null : slice$1.call(_), axis) : tickValues && tickValues.slice();
+  };
+
+  axis.tickFormat = function(_) {
+    return arguments.length ? (tickFormat = _, axis) : tickFormat;
+  };
+
+  axis.tickSize = function(_) {
+    return arguments.length ? (tickSizeInner = tickSizeOuter = +_, axis) : tickSizeInner;
+  };
+
+  axis.tickSizeInner = function(_) {
+    return arguments.length ? (tickSizeInner = +_, axis) : tickSizeInner;
+  };
+
+  axis.tickSizeOuter = function(_) {
+    return arguments.length ? (tickSizeOuter = +_, axis) : tickSizeOuter;
+  };
+
+  axis.tickPadding = function(_) {
+    return arguments.length ? (tickPadding = +_, axis) : tickPadding;
+  };
+
+  return axis;
+}
+
+function axisTop(scale) {
+  return axis(top, scale);
+}
+
+function axisRight(scale) {
+  return axis(right, scale);
+}
+
+function axisBottom(scale) {
+  return axis(bottom, scale);
+}
+
+function axisLeft(scale) {
+  return axis(left, scale);
+}
+
+var noop = {value: function() {}};
+
+function dispatch() {
+  for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
+    if (!(t = arguments[i] + "") || (t in _)) throw new Error("illegal type: " + t);
+    _[t] = [];
+  }
+  return new Dispatch(_);
+}
+
+function Dispatch(_) {
+  this._ = _;
+}
+
+function parseTypenames(typenames, types) {
+  return typenames.trim().split(/^|\s+/).map(function(t) {
+    var name = "", i = t.indexOf(".");
+    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
+    if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
+    return {type: t, name: name};
+  });
+}
+
+Dispatch.prototype = dispatch.prototype = {
+  constructor: Dispatch,
+  on: function(typename, callback) {
+    var _ = this._,
+        T = parseTypenames(typename + "", _),
+        t,
+        i = -1,
+        n = T.length;
+
+    // If no callback was specified, return the callback of the given type and name.
+    if (arguments.length < 2) {
+      while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
+      return;
+    }
+
+    // If a type was specified, set the callback for the given type and name.
+    // Otherwise, if a null callback was specified, remove callbacks of the given name.
+    if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
+    while (++i < n) {
+      if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
+      else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
+    }
+
+    return this;
+  },
+  copy: function() {
+    var copy = {}, _ = this._;
+    for (var t in _) copy[t] = _[t].slice();
+    return new Dispatch(copy);
+  },
+  call: function(type, that) {
+    if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
+    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
+    for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
+  },
+  apply: function(type, that, args) {
+    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
+    for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
+  }
+};
+
+function get(type, name) {
+  for (var i = 0, n = type.length, c; i < n; ++i) {
+    if ((c = type[i]).name === name) {
+      return c.value;
+    }
+  }
+}
+
+function set(type, name, callback) {
+  for (var i = 0, n = type.length; i < n; ++i) {
+    if (type[i].name === name) {
+      type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
+      break;
+    }
+  }
+  if (callback != null) type.push({name: name, value: callback});
+  return type;
+}
+
+var xhtml = "http://www.w3.org/1999/xhtml";
+
+var namespaces = {
+  svg: "http://www.w3.org/2000/svg",
+  xhtml: xhtml,
+  xlink: "http://www.w3.org/1999/xlink",
+  xml: "http://www.w3.org/XML/1998/namespace",
+  xmlns: "http://www.w3.org/2000/xmlns/"
+};
+
+function namespace(name) {
+  var prefix = name += "", i = prefix.indexOf(":");
+  if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1);
+  return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name;
+}
+
+function creatorInherit(name) {
+  return function() {
+    var document = this.ownerDocument,
+        uri = this.namespaceURI;
+    return uri === xhtml && document.documentElement.namespaceURI === xhtml
+        ? document.createElement(name)
+        : document.createElementNS(uri, name);
+  };
+}
+
+function creatorFixed(fullname) {
+  return function() {
+    return this.ownerDocument.createElementNS(fullname.space, fullname.local);
+  };
+}
+
+function creator(name) {
+  var fullname = namespace(name);
+  return (fullname.local
+      ? creatorFixed
+      : creatorInherit)(fullname);
+}
+
+function none() {}
+
+function selector(selector) {
+  return selector == null ? none : function() {
+    return this.querySelector(selector);
+  };
+}
+
+function selection_select(select) {
+  if (typeof select !== "function") select = selector(select);
+
+  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
+      if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
+        if ("__data__" in node) subnode.__data__ = node.__data__;
+        subgroup[i] = subnode;
+      }
+    }
+  }
+
+  return new Selection(subgroups, this._parents);
+}
+
+function empty() {
+  return [];
+}
+
+function selectorAll(selector) {
+  return selector == null ? empty : function() {
+    return this.querySelectorAll(selector);
+  };
+}
+
+function selection_selectAll(select) {
+  if (typeof select !== "function") select = selectorAll(select);
+
+  for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+      if (node = group[i]) {
+        subgroups.push(select.call(node, node.__data__, i, group));
+        parents.push(node);
+      }
+    }
+  }
+
+  return new Selection(subgroups, parents);
+}
+
+function matcher(selector) {
+  return function() {
+    return this.matches(selector);
+  };
+}
+
+function selection_filter(match) {
+  if (typeof match !== "function") match = matcher(match);
+
+  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
+      if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
+        subgroup.push(node);
+      }
+    }
+  }
+
+  return new Selection(subgroups, this._parents);
+}
+
+function sparse(update) {
+  return new Array(update.length);
+}
+
+function selection_enter() {
+  return new Selection(this._enter || this._groups.map(sparse), this._parents);
+}
+
+function EnterNode(parent, datum) {
+  this.ownerDocument = parent.ownerDocument;
+  this.namespaceURI = parent.namespaceURI;
+  this._next = null;
+  this._parent = parent;
+  this.__data__ = datum;
+}
+
+EnterNode.prototype = {
+  constructor: EnterNode,
+  appendChild: function(child) { return this._parent.insertBefore(child, this._next); },
+  insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },
+  querySelector: function(selector) { return this._parent.querySelector(selector); },
+  querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }
+};
+
+function constant$1(x) {
+  return function() {
+    return x;
+  };
+}
+
+var keyPrefix = "$"; // Protect against keys like “__proto__”.
+
+function bindIndex(parent, group, enter, update, exit, data) {
+  var i = 0,
+      node,
+      groupLength = group.length,
+      dataLength = data.length;
+
+  // Put any non-null nodes that fit into update.
+  // Put any null nodes into enter.
+  // Put any remaining data into enter.
+  for (; i < dataLength; ++i) {
+    if (node = group[i]) {
+      node.__data__ = data[i];
+      update[i] = node;
+    } else {
+      enter[i] = new EnterNode(parent, data[i]);
+    }
+  }
+
+  // Put any non-null nodes that don’t fit into exit.
+  for (; i < groupLength; ++i) {
+    if (node = group[i]) {
+      exit[i] = node;
+    }
+  }
+}
+
+function bindKey(parent, group, enter, update, exit, data, key) {
+  var i,
+      node,
+      nodeByKeyValue = {},
+      groupLength = group.length,
+      dataLength = data.length,
+      keyValues = new Array(groupLength),
+      keyValue;
+
+  // Compute the key for each node.
+  // If multiple nodes have the same key, the duplicates are added to exit.
+  for (i = 0; i < groupLength; ++i) {
+    if (node = group[i]) {
+      keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
+      if (keyValue in nodeByKeyValue) {
+        exit[i] = node;
+      } else {
+        nodeByKeyValue[keyValue] = node;
+      }
+    }
+  }
+
+  // Compute the key for each datum.
+  // If there a node associated with this key, join and add it to update.
+  // If there is not (or the key is a duplicate), add it to enter.
+  for (i = 0; i < dataLength; ++i) {
+    keyValue = keyPrefix + key.call(parent, data[i], i, data);
+    if (node = nodeByKeyValue[keyValue]) {
+      update[i] = node;
+      node.__data__ = data[i];
+      nodeByKeyValue[keyValue] = null;
+    } else {
+      enter[i] = new EnterNode(parent, data[i]);
+    }
+  }
+
+  // Add any remaining nodes that were not bound to data to exit.
+  for (i = 0; i < groupLength; ++i) {
+    if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
+      exit[i] = node;
+    }
+  }
+}
+
+function selection_data(value, key) {
+  if (!value) {
+    data = new Array(this.size()), j = -1;
+    this.each(function(d) { data[++j] = d; });
+    return data;
+  }
+
+  var bind = key ? bindKey : bindIndex,
+      parents = this._parents,
+      groups = this._groups;
+
+  if (typeof value !== "function") value = constant$1(value);
+
+  for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
+    var parent = parents[j],
+        group = groups[j],
+        groupLength = group.length,
+        data = value.call(parent, parent && parent.__data__, j, parents),
+        dataLength = data.length,
+        enterGroup = enter[j] = new Array(dataLength),
+        updateGroup = update[j] = new Array(dataLength),
+        exitGroup = exit[j] = new Array(groupLength);
+
+    bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
+
+    // Now connect the enter nodes to their following update node, such that
+    // appendChild can insert the materialized enter node before this node,
+    // rather than at the end of the parent node.
+    for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
+      if (previous = enterGroup[i0]) {
+        if (i0 >= i1) i1 = i0 + 1;
+        while (!(next = updateGroup[i1]) && ++i1 < dataLength);
+        previous._next = next || null;
+      }
+    }
+  }
+
+  update = new Selection(update, parents);
+  update._enter = enter;
+  update._exit = exit;
+  return update;
+}
+
+function selection_exit() {
+  return new Selection(this._exit || this._groups.map(sparse), this._parents);
+}
+
+function selection_join(onenter, onupdate, onexit) {
+  var enter = this.enter(), update = this, exit = this.exit();
+  enter = typeof onenter === "function" ? onenter(enter) : enter.append(onenter + "");
+  if (onupdate != null) update = onupdate(update);
+  if (onexit == null) exit.remove(); else onexit(exit);
+  return enter && update ? enter.merge(update).order() : update;
+}
+
+function selection_merge(selection$$1) {
+
+  for (var groups0 = this._groups, groups1 = selection$$1._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
+    for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
+      if (node = group0[i] || group1[i]) {
+        merge[i] = node;
+      }
+    }
+  }
+
+  for (; j < m0; ++j) {
+    merges[j] = groups0[j];
+  }
+
+  return new Selection(merges, this._parents);
+}
+
+function selection_order() {
+
+  for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
+    for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
+      if (node = group[i]) {
+        if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next);
+        next = node;
+      }
+    }
+  }
+
+  return this;
+}
+
+function selection_sort(compare) {
+  if (!compare) compare = ascending$1;
+
+  function compareNode(a, b) {
+    return a && b ? compare(a.__data__, b.__data__) : !a - !b;
+  }
+
+  for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
+      if (node = group[i]) {
+        sortgroup[i] = node;
+      }
+    }
+    sortgroup.sort(compareNode);
+  }
+
+  return new Selection(sortgroups, this._parents).order();
+}
+
+function ascending$1(a, b) {
+  return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+}
+
+function selection_call() {
+  var callback = arguments[0];
+  arguments[0] = this;
+  callback.apply(null, arguments);
+  return this;
+}
+
+function selection_nodes() {
+  var nodes = new Array(this.size()), i = -1;
+  this.each(function() { nodes[++i] = this; });
+  return nodes;
+}
+
+function selection_node() {
+
+  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
+    for (var group = groups[j], i = 0, n = group.length; i < n; ++i) {
+      var node = group[i];
+      if (node) return node;
+    }
+  }
+
+  return null;
+}
+
+function selection_size() {
+  var size = 0;
+  this.each(function() { ++size; });
+  return size;
+}
+
+function selection_empty() {
+  return !this.node();
+}
+
+function selection_each(callback) {
+
+  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
+    for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
+      if (node = group[i]) callback.call(node, node.__data__, i, group);
+    }
+  }
+
+  return this;
+}
+
+function attrRemove(name) {
+  return function() {
+    this.removeAttribute(name);
+  };
+}
+
+function attrRemoveNS(fullname) {
+  return function() {
+    this.removeAttributeNS(fullname.space, fullname.local);
+  };
+}
+
+function attrConstant(name, value) {
+  return function() {
+    this.setAttribute(name, value);
+  };
+}
+
+function attrConstantNS(fullname, value) {
+  return function() {
+    this.setAttributeNS(fullname.space, fullname.local, value);
+  };
+}
+
+function attrFunction(name, value) {
+  return function() {
+    var v = value.apply(this, arguments);
+    if (v == null) this.removeAttribute(name);
+    else this.setAttribute(name, v);
+  };
+}
+
+function attrFunctionNS(fullname, value) {
+  return function() {
+    var v = value.apply(this, arguments);
+    if (v == null) this.removeAttributeNS(fullname.space, fullname.local);
+    else this.setAttributeNS(fullname.space, fullname.local, v);
+  };
+}
+
+function selection_attr(name, value) {
+  var fullname = namespace(name);
+
+  if (arguments.length < 2) {
+    var node = this.node();
+    return fullname.local
+        ? node.getAttributeNS(fullname.space, fullname.local)
+        : node.getAttribute(fullname);
+  }
+
+  return this.each((value == null
+      ? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === "function"
+      ? (fullname.local ? attrFunctionNS : attrFunction)
+      : (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));
+}
+
+function defaultView(node) {
+  return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node
+      || (node.document && node) // node is a Window
+      || node.defaultView; // node is a Document
+}
+
+function styleRemove(name) {
+  return function() {
+    this.style.removeProperty(name);
+  };
+}
+
+function styleConstant(name, value, priority) {
+  return function() {
+    this.style.setProperty(name, value, priority);
+  };
+}
+
+function styleFunction(name, value, priority) {
+  return function() {
+    var v = value.apply(this, arguments);
+    if (v == null) this.style.removeProperty(name);
+    else this.style.setProperty(name, v, priority);
+  };
+}
+
+function selection_style(name, value, priority) {
+  return arguments.length > 1
+      ? this.each((value == null
+            ? styleRemove : typeof value === "function"
+            ? styleFunction
+            : styleConstant)(name, value, priority == null ? "" : priority))
+      : styleValue(this.node(), name);
+}
+
+function styleValue(node, name) {
+  return node.style.getPropertyValue(name)
+      || defaultView(node).getComputedStyle(node, null).getPropertyValue(name);
+}
+
+function propertyRemove(name) {
+  return function() {
+    delete this[name];
+  };
+}
+
+function propertyConstant(name, value) {
+  return function() {
+    this[name] = value;
+  };
+}
+
+function propertyFunction(name, value) {
+  return function() {
+    var v = value.apply(this, arguments);
+    if (v == null) delete this[name];
+    else this[name] = v;
+  };
+}
+
+function selection_property(name, value) {
+  return arguments.length > 1
+      ? this.each((value == null
+          ? propertyRemove : typeof value === "function"
+          ? propertyFunction
+          : propertyConstant)(name, value))
+      : this.node()[name];
+}
+
+function classArray(string) {
+  return string.trim().split(/^|\s+/);
+}
+
+function classList(node) {
+  return node.classList || new ClassList(node);
+}
+
+function ClassList(node) {
+  this._node = node;
+  this._names = classArray(node.getAttribute("class") || "");
+}
+
+ClassList.prototype = {
+  add: function(name) {
+    var i = this._names.indexOf(name);
+    if (i < 0) {
+      this._names.push(name);
+      this._node.setAttribute("class", this._names.join(" "));
+    }
+  },
+  remove: function(name) {
+    var i = this._names.indexOf(name);
+    if (i >= 0) {
+      this._names.splice(i, 1);
+      this._node.setAttribute("class", this._names.join(" "));
+    }
+  },
+  contains: function(name) {
+    return this._names.indexOf(name) >= 0;
+  }
+};
+
+function classedAdd(node, names) {
+  var list = classList(node), i = -1, n = names.length;
+  while (++i < n) list.add(names[i]);
+}
+
+function classedRemove(node, names) {
+  var list = classList(node), i = -1, n = names.length;
+  while (++i < n) list.remove(names[i]);
+}
+
+function classedTrue(names) {
+  return function() {
+    classedAdd(this, names);
+  };
+}
+
+function classedFalse(names) {
+  return function() {
+    classedRemove(this, names);
+  };
+}
+
+function classedFunction(names, value) {
+  return function() {
+    (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names);
+  };
+}
+
+function selection_classed(name, value) {
+  var names = classArray(name + "");
+
+  if (arguments.length < 2) {
+    var list = classList(this.node()), i = -1, n = names.length;
+    while (++i < n) if (!list.contains(names[i])) return false;
+    return true;
+  }
+
+  return this.each((typeof value === "function"
+      ? classedFunction : value
+      ? classedTrue
+      : classedFalse)(names, value));
+}
+
+function textRemove() {
+  this.textContent = "";
+}
+
+function textConstant(value) {
+  return function() {
+    this.textContent = value;
+  };
+}
+
+function textFunction(value) {
+  return function() {
+    var v = value.apply(this, arguments);
+    this.textContent = v == null ? "" : v;
+  };
+}
+
+function selection_text(value) {
+  return arguments.length
+      ? this.each(value == null
+          ? textRemove : (typeof value === "function"
+          ? textFunction
+          : textConstant)(value))
+      : this.node().textContent;
+}
+
+function htmlRemove() {
+  this.innerHTML = "";
+}
+
+function htmlConstant(value) {
+  return function() {
+    this.innerHTML = value;
+  };
+}
+
+function htmlFunction(value) {
+  return function() {
+    var v = value.apply(this, arguments);
+    this.innerHTML = v == null ? "" : v;
+  };
+}
+
+function selection_html(value) {
+  return arguments.length
+      ? this.each(value == null
+          ? htmlRemove : (typeof value === "function"
+          ? htmlFunction
+          : htmlConstant)(value))
+      : this.node().innerHTML;
+}
+
+function raise() {
+  if (this.nextSibling) this.parentNode.appendChild(this);
+}
+
+function selection_raise() {
+  return this.each(raise);
+}
+
+function lower() {
+  if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild);
+}
+
+function selection_lower() {
+  return this.each(lower);
+}
+
+function selection_append(name) {
+  var create = typeof name === "function" ? name : creator(name);
+  return this.select(function() {
+    return this.appendChild(create.apply(this, arguments));
+  });
+}
+
+function constantNull() {
+  return null;
+}
+
+function selection_insert(name, before) {
+  var create = typeof name === "function" ? name : creator(name),
+      select = before == null ? constantNull : typeof before === "function" ? before : selector(before);
+  return this.select(function() {
+    return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);
+  });
+}
+
+function remove() {
+  var parent = this.parentNode;
+  if (parent) parent.removeChild(this);
+}
+
+function selection_remove() {
+  return this.each(remove);
+}
+
+function selection_cloneShallow() {
+  return this.parentNode.insertBefore(this.cloneNode(false), this.nextSibling);
+}
+
+function selection_cloneDeep() {
+  return this.parentNode.insertBefore(this.cloneNode(true), this.nextSibling);
+}
+
+function selection_clone(deep) {
+  return this.select(deep ? selection_cloneDeep : selection_cloneShallow);
+}
+
+function selection_datum(value) {
+  return arguments.length
+      ? this.property("__data__", value)
+      : this.node().__data__;
+}
+
+var filterEvents = {};
+
+exports.event = null;
+
+if (typeof document !== "undefined") {
+  var element = document.documentElement;
+  if (!("onmouseenter" in element)) {
+    filterEvents = {mouseenter: "mouseover", mouseleave: "mouseout"};
+  }
+}
+
+function filterContextListener(listener, index, group) {
+  listener = contextListener(listener, index, group);
+  return function(event) {
+    var related = event.relatedTarget;
+    if (!related || (related !== this && !(related.compareDocumentPosition(this) & 8))) {
+      listener.call(this, event);
+    }
+  };
+}
+
+function contextListener(listener, index, group) {
+  return function(event1) {
+    var event0 = exports.event; // Events can be reentrant (e.g., focus).
+    exports.event = event1;
+    try {
+      listener.call(this, this.__data__, index, group);
+    } finally {
+      exports.event = event0;
+    }
+  };
+}
+
+function parseTypenames$1(typenames) {
+  return typenames.trim().split(/^|\s+/).map(function(t) {
+    var name = "", i = t.indexOf(".");
+    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
+    return {type: t, name: name};
+  });
+}
+
+function onRemove(typename) {
+  return function() {
+    var on = this.__on;
+    if (!on) return;
+    for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
+      if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
+        this.removeEventListener(o.type, o.listener, o.capture);
+      } else {
+        on[++i] = o;
+      }
+    }
+    if (++i) on.length = i;
+    else delete this.__on;
+  };
+}
+
+function onAdd(typename, value, capture) {
+  var wrap = filterEvents.hasOwnProperty(typename.type) ? filterContextListener : contextListener;
+  return function(d, i, group) {
+    var on = this.__on, o, listener = wrap(value, i, group);
+    if (on) for (var j = 0, m = on.length; j < m; ++j) {
+      if ((o = on[j]).type === typename.type && o.name === typename.name) {
+        this.removeEventListener(o.type, o.listener, o.capture);
+        this.addEventListener(o.type, o.listener = listener, o.capture = capture);
+        o.value = value;
+        return;
+      }
+    }
+    this.addEventListener(typename.type, listener, capture);
+    o = {type: typename.type, name: typename.name, value: value, listener: listener, capture: capture};
+    if (!on) this.__on = [o];
+    else on.push(o);
+  };
+}
+
+function selection_on(typename, value, capture) {
+  var typenames = parseTypenames$1(typename + ""), i, n = typenames.length, t;
+
+  if (arguments.length < 2) {
+    var on = this.node().__on;
+    if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
+      for (i = 0, o = on[j]; i < n; ++i) {
+        if ((t = typenames[i]).type === o.type && t.name === o.name) {
+          return o.value;
+        }
+      }
+    }
+    return;
+  }
+
+  on = value ? onAdd : onRemove;
+  if (capture == null) capture = false;
+  for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
+  return this;
+}
+
+function customEvent(event1, listener, that, args) {
+  var event0 = exports.event;
+  event1.sourceEvent = exports.event;
+  exports.event = event1;
+  try {
+    return listener.apply(that, args);
+  } finally {
+    exports.event = event0;
+  }
+}
+
+function dispatchEvent(node, type, params) {
+  var window = defaultView(node),
+      event = window.CustomEvent;
+
+  if (typeof event === "function") {
+    event = new event(type, params);
+  } else {
+    event = window.document.createEvent("Event");
+    if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
+    else event.initEvent(type, false, false);
+  }
+
+  node.dispatchEvent(event);
+}
+
+function dispatchConstant(type, params) {
+  return function() {
+    return dispatchEvent(this, type, params);
+  };
+}
+
+function dispatchFunction(type, params) {
+  return function() {
+    return dispatchEvent(this, type, params.apply(this, arguments));
+  };
+}
+
+function selection_dispatch(type, params) {
+  return this.each((typeof params === "function"
+      ? dispatchFunction
+      : dispatchConstant)(type, params));
+}
+
+var root = [null];
+
+function Selection(groups, parents) {
+  this._groups = groups;
+  this._parents = parents;
+}
+
+function selection() {
+  return new Selection([[document.documentElement]], root);
+}
+
+Selection.prototype = selection.prototype = {
+  constructor: Selection,
+  select: selection_select,
+  selectAll: selection_selectAll,
+  filter: selection_filter,
+  data: selection_data,
+  enter: selection_enter,
+  exit: selection_exit,
+  join: selection_join,
+  merge: selection_merge,
+  order: selection_order,
+  sort: selection_sort,
+  call: selection_call,
+  nodes: selection_nodes,
+  node: selection_node,
+  size: selection_size,
+  empty: selection_empty,
+  each: selection_each,
+  attr: selection_attr,
+  style: selection_style,
+  property: selection_property,
+  classed: selection_classed,
+  text: selection_text,
+  html: selection_html,
+  raise: selection_raise,
+  lower: selection_lower,
+  append: selection_append,
+  insert: selection_insert,
+  remove: selection_remove,
+  clone: selection_clone,
+  datum: selection_datum,
+  on: selection_on,
+  dispatch: selection_dispatch
+};
+
+function select(selector) {
+  return typeof selector === "string"
+      ? new Selection([[document.querySelector(selector)]], [document.documentElement])
+      : new Selection([[selector]], root);
+}
+
+function create(name) {
+  return select(creator(name).call(document.documentElement));
+}
+
+var nextId = 0;
+
+function local() {
+  return new Local;
+}
+
+function Local() {
+  this._ = "@" + (++nextId).toString(36);
+}
+
+Local.prototype = local.prototype = {
+  constructor: Local,
+  get: function(node) {
+    var id = this._;
+    while (!(id in node)) if (!(node = node.parentNode)) return;
+    return node[id];
+  },
+  set: function(node, value) {
+    return node[this._] = value;
+  },
+  remove: function(node) {
+    return this._ in node && delete node[this._];
+  },
+  toString: function() {
+    return this._;
+  }
+};
+
+function sourceEvent() {
+  var current = exports.event, source;
+  while (source = current.sourceEvent) current = source;
+  return current;
+}
+
+function point(node, event) {
+  var svg = node.ownerSVGElement || node;
+
+  if (svg.createSVGPoint) {
+    var point = svg.createSVGPoint();
+    point.x = event.clientX, point.y = event.clientY;
+    point = point.matrixTransform(node.getScreenCTM().inverse());
+    return [point.x, point.y];
+  }
+
+  var rect = node.getBoundingClientRect();
+  return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
+}
+
+function mouse(node) {
+  var event = sourceEvent();
+  if (event.changedTouches) event = event.changedTouches[0];
+  return point(node, event);
+}
+
+function selectAll(selector) {
+  return typeof selector === "string"
+      ? new Selection([document.querySelectorAll(selector)], [document.documentElement])
+      : new Selection([selector == null ? [] : selector], root);
+}
+
+function touch(node, touches, identifier) {
+  if (arguments.length < 3) identifier = touches, touches = sourceEvent().changedTouches;
+
+  for (var i = 0, n = touches ? touches.length : 0, touch; i < n; ++i) {
+    if ((touch = touches[i]).identifier === identifier) {
+      return point(node, touch);
+    }
+  }
+
+  return null;
+}
+
+function touches(node, touches) {
+  if (touches == null) touches = sourceEvent().touches;
+
+  for (var i = 0, n = touches ? touches.length : 0, points = new Array(n); i < n; ++i) {
+    points[i] = point(node, touches[i]);
+  }
+
+  return points;
+}
+
+function nopropagation() {
+  exports.event.stopImmediatePropagation();
+}
+
+function noevent() {
+  exports.event.preventDefault();
+  exports.event.stopImmediatePropagation();
+}
+
+function dragDisable(view) {
+  var root = view.document.documentElement,
+      selection$$1 = select(view).on("dragstart.drag", noevent, true);
+  if ("onselectstart" in root) {
+    selection$$1.on("selectstart.drag", noevent, true);
+  } else {
+    root.__noselect = root.style.MozUserSelect;
+    root.style.MozUserSelect = "none";
+  }
+}
+
+function yesdrag(view, noclick) {
+  var root = view.document.documentElement,
+      selection$$1 = select(view).on("dragstart.drag", null);
+  if (noclick) {
+    selection$$1.on("click.drag", noevent, true);
+    setTimeout(function() { selection$$1.on("click.drag", null); }, 0);
+  }
+  if ("onselectstart" in root) {
+    selection$$1.on("selectstart.drag", null);
+  } else {
+    root.style.MozUserSelect = root.__noselect;
+    delete root.__noselect;
+  }
+}
+
+function constant$2(x) {
+  return function() {
+    return x;
+  };
+}
+
+function DragEvent(target, type, subject, id, active, x, y, dx, dy, dispatch) {
+  this.target = target;
+  this.type = type;
+  this.subject = subject;
+  this.identifier = id;
+  this.active = active;
+  this.x = x;
+  this.y = y;
+  this.dx = dx;
+  this.dy = dy;
+  this._ = dispatch;
+}
+
+DragEvent.prototype.on = function() {
+  var value = this._.on.apply(this._, arguments);
+  return value === this._ ? this : value;
+};
+
+// Ignore right-click, since that should open the context menu.
+function defaultFilter() {
+  return !exports.event.button;
+}
+
+function defaultContainer() {
+  return this.parentNode;
+}
+
+function defaultSubject(d) {
+  return d == null ? {x: exports.event.x, y: exports.event.y} : d;
+}
+
+function defaultTouchable() {
+  return "ontouchstart" in this;
+}
+
+function drag() {
+  var filter = defaultFilter,
+      container = defaultContainer,
+      subject = defaultSubject,
+      touchable = defaultTouchable,
+      gestures = {},
+      listeners = dispatch("start", "drag", "end"),
+      active = 0,
+      mousedownx,
+      mousedowny,
+      mousemoving,
+      touchending,
+      clickDistance2 = 0;
+
+  function drag(selection$$1) {
+    selection$$1
+        .on("mousedown.drag", mousedowned)
+      .filter(touchable)
+        .on("touchstart.drag", touchstarted)
+        .on("touchmove.drag", touchmoved)
+        .on("touchend.drag touchcancel.drag", touchended)
+        .style("touch-action", "none")
+        .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+  }
+
+  function mousedowned() {
+    if (touchending || !filter.apply(this, arguments)) return;
+    var gesture = beforestart("mouse", container.apply(this, arguments), mouse, this, arguments);
+    if (!gesture) return;
+    select(exports.event.view).on("mousemove.drag", mousemoved, true).on("mouseup.drag", mouseupped, true);
+    dragDisable(exports.event.view);
+    nopropagation();
+    mousemoving = false;
+    mousedownx = exports.event.clientX;
+    mousedowny = exports.event.clientY;
+    gesture("start");
+  }
+
+  function mousemoved() {
+    noevent();
+    if (!mousemoving) {
+      var dx = exports.event.clientX - mousedownx, dy = exports.event.clientY - mousedowny;
+      mousemoving = dx * dx + dy * dy > clickDistance2;
+    }
+    gestures.mouse("drag");
+  }
+
+  function mouseupped() {
+    select(exports.event.view).on("mousemove.drag mouseup.drag", null);
+    yesdrag(exports.event.view, mousemoving);
+    noevent();
+    gestures.mouse("end");
+  }
+
+  function touchstarted() {
+    if (!filter.apply(this, arguments)) return;
+    var touches$$1 = exports.event.changedTouches,
+        c = container.apply(this, arguments),
+        n = touches$$1.length, i, gesture;
+
+    for (i = 0; i < n; ++i) {
+      if (gesture = beforestart(touches$$1[i].identifier, c, touch, this, arguments)) {
+        nopropagation();
+        gesture("start");
+      }
+    }
+  }
+
+  function touchmoved() {
+    var touches$$1 = exports.event.changedTouches,
+        n = touches$$1.length, i, gesture;
+
+    for (i = 0; i < n; ++i) {
+      if (gesture = gestures[touches$$1[i].identifier]) {
+        noevent();
+        gesture("drag");
+      }
+    }
+  }
+
+  function touchended() {
+    var touches$$1 = exports.event.changedTouches,
+        n = touches$$1.length, i, gesture;
+
+    if (touchending) clearTimeout(touchending);
+    touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
+    for (i = 0; i < n; ++i) {
+      if (gesture = gestures[touches$$1[i].identifier]) {
+        nopropagation();
+        gesture("end");
+      }
+    }
+  }
+
+  function beforestart(id, container, point$$1, that, args) {
+    var p = point$$1(container, id), s, dx, dy,
+        sublisteners = listeners.copy();
+
+    if (!customEvent(new DragEvent(drag, "beforestart", s, id, active, p[0], p[1], 0, 0, sublisteners), function() {
+      if ((exports.event.subject = s = subject.apply(that, args)) == null) return false;
+      dx = s.x - p[0] || 0;
+      dy = s.y - p[1] || 0;
+      return true;
+    })) return;
+
+    return function gesture(type) {
+      var p0 = p, n;
+      switch (type) {
+        case "start": gestures[id] = gesture, n = active++; break;
+        case "end": delete gestures[id], --active; // nobreak
+        case "drag": p = point$$1(container, id), n = active; break;
+      }
+      customEvent(new DragEvent(drag, type, s, id, n, p[0] + dx, p[1] + dy, p[0] - p0[0], p[1] - p0[1], sublisteners), sublisteners.apply, sublisteners, [type, that, args]);
+    };
+  }
+
+  drag.filter = function(_) {
+    return arguments.length ? (filter = typeof _ === "function" ? _ : constant$2(!!_), drag) : filter;
+  };
+
+  drag.container = function(_) {
+    return arguments.length ? (container = typeof _ === "function" ? _ : constant$2(_), drag) : container;
+  };
+
+  drag.subject = function(_) {
+    return arguments.length ? (subject = typeof _ === "function" ? _ : constant$2(_), drag) : subject;
+  };
+
+  drag.touchable = function(_) {
+    return arguments.length ? (touchable = typeof _ === "function" ? _ : constant$2(!!_), drag) : touchable;
+  };
+
+  drag.on = function() {
+    var value = listeners.on.apply(listeners, arguments);
+    return value === listeners ? drag : value;
+  };
+
+  drag.clickDistance = function(_) {
+    return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
+  };
+
+  return drag;
+}
+
+function define(constructor, factory, prototype) {
+  constructor.prototype = factory.prototype = prototype;
+  prototype.constructor = constructor;
+}
+
+function extend(parent, definition) {
+  var prototype = Object.create(parent.prototype);
+  for (var key in definition) prototype[key] = definition[key];
+  return prototype;
+}
+
+function Color() {}
+
+var darker = 0.7;
+var brighter = 1 / darker;
+
+var reI = "\\s*([+-]?\\d+)\\s*",
+    reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",
+    reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",
+    reHex3 = /^#([0-9a-f]{3})$/,
+    reHex6 = /^#([0-9a-f]{6})$/,
+    reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"),
+    reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"),
+    reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"),
+    reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"),
+    reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"),
+    reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$");
+
+var named = {
+  aliceblue: 0xf0f8ff,
+  antiquewhite: 0xfaebd7,
+  aqua: 0x00ffff,
+  aquamarine: 0x7fffd4,
+  azure: 0xf0ffff,
+  beige: 0xf5f5dc,
+  bisque: 0xffe4c4,
+  black: 0x000000,
+  blanchedalmond: 0xffebcd,
+  blue: 0x0000ff,
+  blueviolet: 0x8a2be2,
+  brown: 0xa52a2a,
+  burlywood: 0xdeb887,
+  cadetblue: 0x5f9ea0,
+  chartreuse: 0x7fff00,
+  chocolate: 0xd2691e,
+  coral: 0xff7f50,
+  cornflowerblue: 0x6495ed,
+  cornsilk: 0xfff8dc,
+  crimson: 0xdc143c,
+  cyan: 0x00ffff,
+  darkblue: 0x00008b,
+  darkcyan: 0x008b8b,
+  darkgoldenrod: 0xb8860b,
+  darkgray: 0xa9a9a9,
+  darkgreen: 0x006400,
+  darkgrey: 0xa9a9a9,
+  darkkhaki: 0xbdb76b,
+  darkmagenta: 0x8b008b,
+  darkolivegreen: 0x556b2f,
+  darkorange: 0xff8c00,
+  darkorchid: 0x9932cc,
+  darkred: 0x8b0000,
+  darksalmon: 0xe9967a,
+  darkseagreen: 0x8fbc8f,
+  darkslateblue: 0x483d8b,
+  darkslategray: 0x2f4f4f,
+  darkslategrey: 0x2f4f4f,
+  darkturquoise: 0x00ced1,
+  darkviolet: 0x9400d3,
+  deeppink: 0xff1493,
+  deepskyblue: 0x00bfff,
+  dimgray: 0x696969,
+  dimgrey: 0x696969,
+  dodgerblue: 0x1e90ff,
+  firebrick: 0xb22222,
+  floralwhite: 0xfffaf0,
+  forestgreen: 0x228b22,
+  fuchsia: 0xff00ff,
+  gainsboro: 0xdcdcdc,
+  ghostwhite: 0xf8f8ff,
+  gold: 0xffd700,
+  goldenrod: 0xdaa520,
+  gray: 0x808080,
+  green: 0x008000,
+  greenyellow: 0xadff2f,
+  grey: 0x808080,
+  honeydew: 0xf0fff0,
+  hotpink: 0xff69b4,
+  indianred: 0xcd5c5c,
+  indigo: 0x4b0082,
+  ivory: 0xfffff0,
+  khaki: 0xf0e68c,
+  lavender: 0xe6e6fa,
+  lavenderblush: 0xfff0f5,
+  lawngreen: 0x7cfc00,
+  lemonchiffon: 0xfffacd,
+  lightblue: 0xadd8e6,
+  lightcoral: 0xf08080,
+  lightcyan: 0xe0ffff,
+  lightgoldenrodyellow: 0xfafad2,
+  lightgray: 0xd3d3d3,
+  lightgreen: 0x90ee90,
+  lightgrey: 0xd3d3d3,
+  lightpink: 0xffb6c1,
+  lightsalmon: 0xffa07a,
+  lightseagreen: 0x20b2aa,
+  lightskyblue: 0x87cefa,
+  lightslategray: 0x778899,
+  lightslategrey: 0x778899,
+  lightsteelblue: 0xb0c4de,
+  lightyellow: 0xffffe0,
+  lime: 0x00ff00,
+  limegreen: 0x32cd32,
+  linen: 0xfaf0e6,
+  magenta: 0xff00ff,
+  maroon: 0x800000,
+  mediumaquamarine: 0x66cdaa,
+  mediumblue: 0x0000cd,
+  mediumorchid: 0xba55d3,
+  mediumpurple: 0x9370db,
+  mediumseagreen: 0x3cb371,
+  mediumslateblue: 0x7b68ee,
+  mediumspringgreen: 0x00fa9a,
+  mediumturquoise: 0x48d1cc,
+  mediumvioletred: 0xc71585,
+  midnightblue: 0x191970,
+  mintcream: 0xf5fffa,
+  mistyrose: 0xffe4e1,
+  moccasin: 0xffe4b5,
+  navajowhite: 0xffdead,
+  navy: 0x000080,
+  oldlace: 0xfdf5e6,
+  olive: 0x808000,
+  olivedrab: 0x6b8e23,
+  orange: 0xffa500,
+  orangered: 0xff4500,
+  orchid: 0xda70d6,
+  palegoldenrod: 0xeee8aa,
+  palegreen: 0x98fb98,
+  paleturquoise: 0xafeeee,
+  palevioletred: 0xdb7093,
+  papayawhip: 0xffefd5,
+  peachpuff: 0xffdab9,
+  peru: 0xcd853f,
+  pink: 0xffc0cb,
+  plum: 0xdda0dd,
+  powderblue: 0xb0e0e6,
+  purple: 0x800080,
+  rebeccapurple: 0x663399,
+  red: 0xff0000,
+  rosybrown: 0xbc8f8f,
+  royalblue: 0x4169e1,
+  saddlebrown: 0x8b4513,
+  salmon: 0xfa8072,
+  sandybrown: 0xf4a460,
+  seagreen: 0x2e8b57,
+  seashell: 0xfff5ee,
+  sienna: 0xa0522d,
+  silver: 0xc0c0c0,
+  skyblue: 0x87ceeb,
+  slateblue: 0x6a5acd,
+  slategray: 0x708090,
+  slategrey: 0x708090,
+  snow: 0xfffafa,
+  springgreen: 0x00ff7f,
+  steelblue: 0x4682b4,
+  tan: 0xd2b48c,
+  teal: 0x008080,
+  thistle: 0xd8bfd8,
+  tomato: 0xff6347,
+  turquoise: 0x40e0d0,
+  violet: 0xee82ee,
+  wheat: 0xf5deb3,
+  white: 0xffffff,
+  whitesmoke: 0xf5f5f5,
+  yellow: 0xffff00,
+  yellowgreen: 0x9acd32
+};
+
+define(Color, color, {
+  displayable: function() {
+    return this.rgb().displayable();
+  },
+  hex: function() {
+    return this.rgb().hex();
+  },
+  toString: function() {
+    return this.rgb() + "";
+  }
+});
+
+function color(format) {
+  var m;
+  format = (format + "").trim().toLowerCase();
+  return (m = reHex3.exec(format)) ? (m = parseInt(m[1], 16), new Rgb((m >> 8 & 0xf) | (m >> 4 & 0x0f0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1)) // #f00
+      : (m = reHex6.exec(format)) ? rgbn(parseInt(m[1], 16)) // #ff0000
+      : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)
+      : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)
+      : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)
+      : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)
+      : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)
+      : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)
+      : named.hasOwnProperty(format) ? rgbn(named[format])
+      : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0)
+      : null;
+}
+
+function rgbn(n) {
+  return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);
+}
+
+function rgba(r, g, b, a) {
+  if (a <= 0) r = g = b = NaN;
+  return new Rgb(r, g, b, a);
+}
+
+function rgbConvert(o) {
+  if (!(o instanceof Color)) o = color(o);
+  if (!o) return new Rgb;
+  o = o.rgb();
+  return new Rgb(o.r, o.g, o.b, o.opacity);
+}
+
+function rgb(r, g, b, opacity) {
+  return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);
+}
+
+function Rgb(r, g, b, opacity) {
+  this.r = +r;
+  this.g = +g;
+  this.b = +b;
+  this.opacity = +opacity;
+}
+
+define(Rgb, rgb, extend(Color, {
+  brighter: function(k) {
+    k = k == null ? brighter : Math.pow(brighter, k);
+    return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
+  },
+  darker: function(k) {
+    k = k == null ? darker : Math.pow(darker, k);
+    return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
+  },
+  rgb: function() {
+    return this;
+  },
+  displayable: function() {
+    return (0 <= this.r && this.r <= 255)
+        && (0 <= this.g && this.g <= 255)
+        && (0 <= this.b && this.b <= 255)
+        && (0 <= this.opacity && this.opacity <= 1);
+  },
+  hex: function() {
+    return "#" + hex(this.r) + hex(this.g) + hex(this.b);
+  },
+  toString: function() {
+    var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
+    return (a === 1 ? "rgb(" : "rgba(")
+        + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", "
+        + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", "
+        + Math.max(0, Math.min(255, Math.round(this.b) || 0))
+        + (a === 1 ? ")" : ", " + a + ")");
+  }
+}));
+
+function hex(value) {
+  value = Math.max(0, Math.min(255, Math.round(value) || 0));
+  return (value < 16 ? "0" : "") + value.toString(16);
+}
+
+function hsla(h, s, l, a) {
+  if (a <= 0) h = s = l = NaN;
+  else if (l <= 0 || l >= 1) h = s = NaN;
+  else if (s <= 0) h = NaN;
+  return new Hsl(h, s, l, a);
+}
+
+function hslConvert(o) {
+  if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);
+  if (!(o instanceof Color)) o = color(o);
+  if (!o) return new Hsl;
+  if (o instanceof Hsl) return o;
+  o = o.rgb();
+  var r = o.r / 255,
+      g = o.g / 255,
+      b = o.b / 255,
+      min = Math.min(r, g, b),
+      max = Math.max(r, g, b),
+      h = NaN,
+      s = max - min,
+      l = (max + min) / 2;
+  if (s) {
+    if (r === max) h = (g - b) / s + (g < b) * 6;
+    else if (g === max) h = (b - r) / s + 2;
+    else h = (r - g) / s + 4;
+    s /= l < 0.5 ? max + min : 2 - max - min;
+    h *= 60;
+  } else {
+    s = l > 0 && l < 1 ? 0 : h;
+  }
+  return new Hsl(h, s, l, o.opacity);
+}
+
+function hsl(h, s, l, opacity) {
+  return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);
+}
+
+function Hsl(h, s, l, opacity) {
+  this.h = +h;
+  this.s = +s;
+  this.l = +l;
+  this.opacity = +opacity;
+}
+
+define(Hsl, hsl, extend(Color, {
+  brighter: function(k) {
+    k = k == null ? brighter : Math.pow(brighter, k);
+    return new Hsl(this.h, this.s, this.l * k, this.opacity);
+  },
+  darker: function(k) {
+    k = k == null ? darker : Math.pow(darker, k);
+    return new Hsl(this.h, this.s, this.l * k, this.opacity);
+  },
+  rgb: function() {
+    var h = this.h % 360 + (this.h < 0) * 360,
+        s = isNaN(h) || isNaN(this.s) ? 0 : this.s,
+        l = this.l,
+        m2 = l + (l < 0.5 ? l : 1 - l) * s,
+        m1 = 2 * l - m2;
+    return new Rgb(
+      hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),
+      hsl2rgb(h, m1, m2),
+      hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),
+      this.opacity
+    );
+  },
+  displayable: function() {
+    return (0 <= this.s && this.s <= 1 || isNaN(this.s))
+        && (0 <= this.l && this.l <= 1)
+        && (0 <= this.opacity && this.opacity <= 1);
+  }
+}));
+
+/* From FvD 13.37, CSS Color Module Level 3 */
+function hsl2rgb(h, m1, m2) {
+  return (h < 60 ? m1 + (m2 - m1) * h / 60
+      : h < 180 ? m2
+      : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60
+      : m1) * 255;
+}
+
+var deg2rad = Math.PI / 180;
+var rad2deg = 180 / Math.PI;
+
+// https://beta.observablehq.com/@mbostock/lab-and-rgb
+var K = 18,
+    Xn = 0.96422,
+    Yn = 1,
+    Zn = 0.82521,
+    t0 = 4 / 29,
+    t1 = 6 / 29,
+    t2 = 3 * t1 * t1,
+    t3 = t1 * t1 * t1;
+
+function labConvert(o) {
+  if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);
+  if (o instanceof Hcl) {
+    if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity);
+    var h = o.h * deg2rad;
+    return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);
+  }
+  if (!(o instanceof Rgb)) o = rgbConvert(o);
+  var r = rgb2lrgb(o.r),
+      g = rgb2lrgb(o.g),
+      b = rgb2lrgb(o.b),
+      y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn), x, z;
+  if (r === g && g === b) x = z = y; else {
+    x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
+    z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
+  }
+  return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);
+}
+
+function gray(l, opacity) {
+  return new Lab(l, 0, 0, opacity == null ? 1 : opacity);
+}
+
+function lab(l, a, b, opacity) {
+  return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);
+}
+
+function Lab(l, a, b, opacity) {
+  this.l = +l;
+  this.a = +a;
+  this.b = +b;
+  this.opacity = +opacity;
+}
+
+define(Lab, lab, extend(Color, {
+  brighter: function(k) {
+    return new Lab(this.l + K * (k == null ? 1 : k), this.a, this.b, this.opacity);
+  },
+  darker: function(k) {
+    return new Lab(this.l - K * (k == null ? 1 : k), this.a, this.b, this.opacity);
+  },
+  rgb: function() {
+    var y = (this.l + 16) / 116,
+        x = isNaN(this.a) ? y : y + this.a / 500,
+        z = isNaN(this.b) ? y : y - this.b / 200;
+    x = Xn * lab2xyz(x);
+    y = Yn * lab2xyz(y);
+    z = Zn * lab2xyz(z);
+    return new Rgb(
+      lrgb2rgb( 3.1338561 * x - 1.6168667 * y - 0.4906146 * z),
+      lrgb2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),
+      lrgb2rgb( 0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
+      this.opacity
+    );
+  }
+}));
+
+function xyz2lab(t) {
+  return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;
+}
+
+function lab2xyz(t) {
+  return t > t1 ? t * t * t : t2 * (t - t0);
+}
+
+function lrgb2rgb(x) {
+  return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
+}
+
+function rgb2lrgb(x) {
+  return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+}
+
+function hclConvert(o) {
+  if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity);
+  if (!(o instanceof Lab)) o = labConvert(o);
+  if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0, o.l, o.opacity);
+  var h = Math.atan2(o.b, o.a) * rad2deg;
+  return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity);
+}
+
+function lch(l, c, h, opacity) {
+  return arguments.length === 1 ? hclConvert(l) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
+}
+
+function hcl(h, c, l, opacity) {
+  return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
+}
+
+function Hcl(h, c, l, opacity) {
+  this.h = +h;
+  this.c = +c;
+  this.l = +l;
+  this.opacity = +opacity;
+}
+
+define(Hcl, hcl, extend(Color, {
+  brighter: function(k) {
+    return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity);
+  },
+  darker: function(k) {
+    return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity);
+  },
+  rgb: function() {
+    return labConvert(this).rgb();
+  }
+}));
+
+var A = -0.14861,
+    B = +1.78277,
+    C = -0.29227,
+    D = -0.90649,
+    E = +1.97294,
+    ED = E * D,
+    EB = E * B,
+    BC_DA = B * C - D * A;
+
+function cubehelixConvert(o) {
+  if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);
+  if (!(o instanceof Rgb)) o = rgbConvert(o);
+  var r = o.r / 255,
+      g = o.g / 255,
+      b = o.b / 255,
+      l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),
+      bl = b - l,
+      k = (E * (g - l) - C * bl) / D,
+      s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1
+      h = s ? Math.atan2(k, bl) * rad2deg - 120 : NaN;
+  return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);
+}
+
+function cubehelix(h, s, l, opacity) {
+  return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);
+}
+
+function Cubehelix(h, s, l, opacity) {
+  this.h = +h;
+  this.s = +s;
+  this.l = +l;
+  this.opacity = +opacity;
+}
+
+define(Cubehelix, cubehelix, extend(Color, {
+  brighter: function(k) {
+    k = k == null ? brighter : Math.pow(brighter, k);
+    return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
+  },
+  darker: function(k) {
+    k = k == null ? darker : Math.pow(darker, k);
+    return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
+  },
+  rgb: function() {
+    var h = isNaN(this.h) ? 0 : (this.h + 120) * deg2rad,
+        l = +this.l,
+        a = isNaN(this.s) ? 0 : this.s * l * (1 - l),
+        cosh = Math.cos(h),
+        sinh = Math.sin(h);
+    return new Rgb(
+      255 * (l + a * (A * cosh + B * sinh)),
+      255 * (l + a * (C * cosh + D * sinh)),
+      255 * (l + a * (E * cosh)),
+      this.opacity
+    );
+  }
+}));
+
+function basis(t1, v0, v1, v2, v3) {
+  var t2 = t1 * t1, t3 = t2 * t1;
+  return ((1 - 3 * t1 + 3 * t2 - t3) * v0
+      + (4 - 6 * t2 + 3 * t3) * v1
+      + (1 + 3 * t1 + 3 * t2 - 3 * t3) * v2
+      + t3 * v3) / 6;
+}
+
+function basis$1(values) {
+  var n = values.length - 1;
+  return function(t) {
+    var i = t <= 0 ? (t = 0) : t >= 1 ? (t = 1, n - 1) : Math.floor(t * n),
+        v1 = values[i],
+        v2 = values[i + 1],
+        v0 = i > 0 ? values[i - 1] : 2 * v1 - v2,
+        v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1;
+    return basis((t - i / n) * n, v0, v1, v2, v3);
+  };
+}
+
+function basisClosed(values) {
+  var n = values.length;
+  return function(t) {
+    var i = Math.floor(((t %= 1) < 0 ? ++t : t) * n),
+        v0 = values[(i + n - 1) % n],
+        v1 = values[i % n],
+        v2 = values[(i + 1) % n],
+        v3 = values[(i + 2) % n];
+    return basis((t - i / n) * n, v0, v1, v2, v3);
+  };
+}
+
+function constant$3(x) {
+  return function() {
+    return x;
+  };
+}
+
+function linear(a, d) {
+  return function(t) {
+    return a + t * d;
+  };
+}
+
+function exponential(a, b, y) {
+  return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) {
+    return Math.pow(a + t * b, y);
+  };
+}
+
+function hue(a, b) {
+  var d = b - a;
+  return d ? linear(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) : constant$3(isNaN(a) ? b : a);
+}
+
+function gamma(y) {
+  return (y = +y) === 1 ? nogamma : function(a, b) {
+    return b - a ? exponential(a, b, y) : constant$3(isNaN(a) ? b : a);
+  };
+}
+
+function nogamma(a, b) {
+  var d = b - a;
+  return d ? linear(a, d) : constant$3(isNaN(a) ? b : a);
+}
+
+var interpolateRgb = (function rgbGamma(y) {
+  var color$$1 = gamma(y);
+
+  function rgb$$1(start, end) {
+    var r = color$$1((start = rgb(start)).r, (end = rgb(end)).r),
+        g = color$$1(start.g, end.g),
+        b = color$$1(start.b, end.b),
+        opacity = nogamma(start.opacity, end.opacity);
+    return function(t) {
+      start.r = r(t);
+      start.g = g(t);
+      start.b = b(t);
+      start.opacity = opacity(t);
+      return start + "";
+    };
+  }
+
+  rgb$$1.gamma = rgbGamma;
+
+  return rgb$$1;
+})(1);
+
+function rgbSpline(spline) {
+  return function(colors) {
+    var n = colors.length,
+        r = new Array(n),
+        g = new Array(n),
+        b = new Array(n),
+        i, color$$1;
+    for (i = 0; i < n; ++i) {
+      color$$1 = rgb(colors[i]);
+      r[i] = color$$1.r || 0;
+      g[i] = color$$1.g || 0;
+      b[i] = color$$1.b || 0;
+    }
+    r = spline(r);
+    g = spline(g);
+    b = spline(b);
+    color$$1.opacity = 1;
+    return function(t) {
+      color$$1.r = r(t);
+      color$$1.g = g(t);
+      color$$1.b = b(t);
+      return color$$1 + "";
+    };
+  };
+}
+
+var rgbBasis = rgbSpline(basis$1);
+var rgbBasisClosed = rgbSpline(basisClosed);
+
+function array$1(a, b) {
+  var nb = b ? b.length : 0,
+      na = a ? Math.min(nb, a.length) : 0,
+      x = new Array(na),
+      c = new Array(nb),
+      i;
+
+  for (i = 0; i < na; ++i) x[i] = interpolateValue(a[i], b[i]);
+  for (; i < nb; ++i) c[i] = b[i];
+
+  return function(t) {
+    for (i = 0; i < na; ++i) c[i] = x[i](t);
+    return c;
+  };
+}
+
+function date(a, b) {
+  var d = new Date;
+  return a = +a, b -= a, function(t) {
+    return d.setTime(a + b * t), d;
+  };
+}
+
+function interpolateNumber(a, b) {
+  return a = +a, b -= a, function(t) {
+    return a + b * t;
+  };
+}
+
+function object(a, b) {
+  var i = {},
+      c = {},
+      k;
+
+  if (a === null || typeof a !== "object") a = {};
+  if (b === null || typeof b !== "object") b = {};
+
+  for (k in b) {
+    if (k in a) {
+      i[k] = interpolateValue(a[k], b[k]);
+    } else {
+      c[k] = b[k];
+    }
+  }
+
+  return function(t) {
+    for (k in i) c[k] = i[k](t);
+    return c;
+  };
+}
+
+var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,
+    reB = new RegExp(reA.source, "g");
+
+function zero(b) {
+  return function() {
+    return b;
+  };
+}
+
+function one(b) {
+  return function(t) {
+    return b(t) + "";
+  };
+}
+
+function interpolateString(a, b) {
+  var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b
+      am, // current match in a
+      bm, // current match in b
+      bs, // string preceding current number in b, if any
+      i = -1, // index in s
+      s = [], // string constants and placeholders
+      q = []; // number interpolators
+
+  // Coerce inputs to strings.
+  a = a + "", b = b + "";
+
+  // Interpolate pairs of numbers in a & b.
+  while ((am = reA.exec(a))
+      && (bm = reB.exec(b))) {
+    if ((bs = bm.index) > bi) { // a string precedes the next number in b
+      bs = b.slice(bi, bs);
+      if (s[i]) s[i] += bs; // coalesce with previous string
+      else s[++i] = bs;
+    }
+    if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match
+      if (s[i]) s[i] += bm; // coalesce with previous string
+      else s[++i] = bm;
+    } else { // interpolate non-matching numbers
+      s[++i] = null;
+      q.push({i: i, x: interpolateNumber(am, bm)});
+    }
+    bi = reB.lastIndex;
+  }
+
+  // Add remains of b.
+  if (bi < b.length) {
+    bs = b.slice(bi);
+    if (s[i]) s[i] += bs; // coalesce with previous string
+    else s[++i] = bs;
+  }
+
+  // Special optimization for only a single match.
+  // Otherwise, interpolate each of the numbers and rejoin the string.
+  return s.length < 2 ? (q[0]
+      ? one(q[0].x)
+      : zero(b))
+      : (b = q.length, function(t) {
+          for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);
+          return s.join("");
+        });
+}
+
+function interpolateValue(a, b) {
+  var t = typeof b, c;
+  return b == null || t === "boolean" ? constant$3(b)
+      : (t === "number" ? interpolateNumber
+      : t === "string" ? ((c = color(b)) ? (b = c, interpolateRgb) : interpolateString)
+      : b instanceof color ? interpolateRgb
+      : b instanceof Date ? date
+      : Array.isArray(b) ? array$1
+      : typeof b.valueOf !== "function" && typeof b.toString !== "function" || isNaN(b) ? object
+      : interpolateNumber)(a, b);
+}
+
+function discrete(range) {
+  var n = range.length;
+  return function(t) {
+    return range[Math.max(0, Math.min(n - 1, Math.floor(t * n)))];
+  };
+}
+
+function hue$1(a, b) {
+  var i = hue(+a, +b);
+  return function(t) {
+    var x = i(t);
+    return x - 360 * Math.floor(x / 360);
+  };
+}
+
+function interpolateRound(a, b) {
+  return a = +a, b -= a, function(t) {
+    return Math.round(a + b * t);
+  };
+}
+
+var degrees = 180 / Math.PI;
+
+var identity$2 = {
+  translateX: 0,
+  translateY: 0,
+  rotate: 0,
+  skewX: 0,
+  scaleX: 1,
+  scaleY: 1
+};
+
+function decompose(a, b, c, d, e, f) {
+  var scaleX, scaleY, skewX;
+  if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;
+  if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;
+  if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;
+  if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;
+  return {
+    translateX: e,
+    translateY: f,
+    rotate: Math.atan2(b, a) * degrees,
+    skewX: Math.atan(skewX) * degrees,
+    scaleX: scaleX,
+    scaleY: scaleY
+  };
+}
+
+var cssNode,
+    cssRoot,
+    cssView,
+    svgNode;
+
+function parseCss(value) {
+  if (value === "none") return identity$2;
+  if (!cssNode) cssNode = document.createElement("DIV"), cssRoot = document.documentElement, cssView = document.defaultView;
+  cssNode.style.transform = value;
+  value = cssView.getComputedStyle(cssRoot.appendChild(cssNode), null).getPropertyValue("transform");
+  cssRoot.removeChild(cssNode);
+  value = value.slice(7, -1).split(",");
+  return decompose(+value[0], +value[1], +value[2], +value[3], +value[4], +value[5]);
+}
+
+function parseSvg(value) {
+  if (value == null) return identity$2;
+  if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
+  svgNode.setAttribute("transform", value);
+  if (!(value = svgNode.transform.baseVal.consolidate())) return identity$2;
+  value = value.matrix;
+  return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
+}
+
+function interpolateTransform(parse, pxComma, pxParen, degParen) {
+
+  function pop(s) {
+    return s.length ? s.pop() + " " : "";
+  }
+
+  function translate(xa, ya, xb, yb, s, q) {
+    if (xa !== xb || ya !== yb) {
+      var i = s.push("translate(", null, pxComma, null, pxParen);
+      q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
+    } else if (xb || yb) {
+      s.push("translate(" + xb + pxComma + yb + pxParen);
+    }
+  }
+
+  function rotate(a, b, s, q) {
+    if (a !== b) {
+      if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path
+      q.push({i: s.push(pop(s) + "rotate(", null, degParen) - 2, x: interpolateNumber(a, b)});
+    } else if (b) {
+      s.push(pop(s) + "rotate(" + b + degParen);
+    }
+  }
+
+  function skewX(a, b, s, q) {
+    if (a !== b) {
+      q.push({i: s.push(pop(s) + "skewX(", null, degParen) - 2, x: interpolateNumber(a, b)});
+    } else if (b) {
+      s.push(pop(s) + "skewX(" + b + degParen);
+    }
+  }
+
+  function scale(xa, ya, xb, yb, s, q) {
+    if (xa !== xb || ya !== yb) {
+      var i = s.push(pop(s) + "scale(", null, ",", null, ")");
+      q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
+    } else if (xb !== 1 || yb !== 1) {
+      s.push(pop(s) + "scale(" + xb + "," + yb + ")");
+    }
+  }
+
+  return function(a, b) {
+    var s = [], // string constants and placeholders
+        q = []; // number interpolators
+    a = parse(a), b = parse(b);
+    translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q);
+    rotate(a.rotate, b.rotate, s, q);
+    skewX(a.skewX, b.skewX, s, q);
+    scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q);
+    a = b = null; // gc
+    return function(t) {
+      var i = -1, n = q.length, o;
+      while (++i < n) s[(o = q[i]).i] = o.x(t);
+      return s.join("");
+    };
+  };
+}
+
+var interpolateTransformCss = interpolateTransform(parseCss, "px, ", "px)", "deg)");
+var interpolateTransformSvg = interpolateTransform(parseSvg, ", ", ")", ")");
+
+var rho = Math.SQRT2,
+    rho2 = 2,
+    rho4 = 4,
+    epsilon2 = 1e-12;
+
+function cosh(x) {
+  return ((x = Math.exp(x)) + 1 / x) / 2;
+}
+
+function sinh(x) {
+  return ((x = Math.exp(x)) - 1 / x) / 2;
+}
+
+function tanh(x) {
+  return ((x = Math.exp(2 * x)) - 1) / (x + 1);
+}
+
+// p0 = [ux0, uy0, w0]
+// p1 = [ux1, uy1, w1]
+function interpolateZoom(p0, p1) {
+  var ux0 = p0[0], uy0 = p0[1], w0 = p0[2],
+      ux1 = p1[0], uy1 = p1[1], w1 = p1[2],
+      dx = ux1 - ux0,
+      dy = uy1 - uy0,
+      d2 = dx * dx + dy * dy,
+      i,
+      S;
+
+  // Special case for u0 ≅ u1.
+  if (d2 < epsilon2) {
+    S = Math.log(w1 / w0) / rho;
+    i = function(t) {
+      return [
+        ux0 + t * dx,
+        uy0 + t * dy,
+        w0 * Math.exp(rho * t * S)
+      ];
+    };
+  }
+
+  // General case.
+  else {
+    var d1 = Math.sqrt(d2),
+        b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1),
+        b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1),
+        r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0),
+        r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);
+    S = (r1 - r0) / rho;
+    i = function(t) {
+      var s = t * S,
+          coshr0 = cosh(r0),
+          u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0));
+      return [
+        ux0 + u * dx,
+        uy0 + u * dy,
+        w0 * coshr0 / cosh(rho * s + r0)
+      ];
+    };
+  }
+
+  i.duration = S * 1000;
+
+  return i;
+}
+
+function hsl$1(hue$$1) {
+  return function(start, end) {
+    var h = hue$$1((start = hsl(start)).h, (end = hsl(end)).h),
+        s = nogamma(start.s, end.s),
+        l = nogamma(start.l, end.l),
+        opacity = nogamma(start.opacity, end.opacity);
+    return function(t) {
+      start.h = h(t);
+      start.s = s(t);
+      start.l = l(t);
+      start.opacity = opacity(t);
+      return start + "";
+    };
+  }
+}
+
+var hsl$2 = hsl$1(hue);
+var hslLong = hsl$1(nogamma);
+
+function lab$1(start, end) {
+  var l = nogamma((start = lab(start)).l, (end = lab(end)).l),
+      a = nogamma(start.a, end.a),
+      b = nogamma(start.b, end.b),
+      opacity = nogamma(start.opacity, end.opacity);
+  return function(t) {
+    start.l = l(t);
+    start.a = a(t);
+    start.b = b(t);
+    start.opacity = opacity(t);
+    return start + "";
+  };
+}
+
+function hcl$1(hue$$1) {
+  return function(start, end) {
+    var h = hue$$1((start = hcl(start)).h, (end = hcl(end)).h),
+        c = nogamma(start.c, end.c),
+        l = nogamma(start.l, end.l),
+        opacity = nogamma(start.opacity, end.opacity);
+    return function(t) {
+      start.h = h(t);
+      start.c = c(t);
+      start.l = l(t);
+      start.opacity = opacity(t);
+      return start + "";
+    };
+  }
+}
+
+var hcl$2 = hcl$1(hue);
+var hclLong = hcl$1(nogamma);
+
+function cubehelix$1(hue$$1) {
+  return (function cubehelixGamma(y) {
+    y = +y;
+
+    function cubehelix$$1(start, end) {
+      var h = hue$$1((start = cubehelix(start)).h, (end = cubehelix(end)).h),
+          s = nogamma(start.s, end.s),
+          l = nogamma(start.l, end.l),
+          opacity = nogamma(start.opacity, end.opacity);
+      return function(t) {
+        start.h = h(t);
+        start.s = s(t);
+        start.l = l(Math.pow(t, y));
+        start.opacity = opacity(t);
+        return start + "";
+      };
+    }
+
+    cubehelix$$1.gamma = cubehelixGamma;
+
+    return cubehelix$$1;
+  })(1);
+}
+
+var cubehelix$2 = cubehelix$1(hue);
+var cubehelixLong = cubehelix$1(nogamma);
+
+function piecewise(interpolate, values) {
+  var i = 0, n = values.length - 1, v = values[0], I = new Array(n < 0 ? 0 : n);
+  while (i < n) I[i] = interpolate(v, v = values[++i]);
+  return function(t) {
+    var i = Math.max(0, Math.min(n - 1, Math.floor(t *= n)));
+    return I[i](t - i);
+  };
+}
+
+function quantize(interpolator, n) {
+  var samples = new Array(n);
+  for (var i = 0; i < n; ++i) samples[i] = interpolator(i / (n - 1));
+  return samples;
+}
+
+var frame = 0, // is an animation frame pending?
+    timeout = 0, // is a timeout pending?
+    interval = 0, // are any timers active?
+    pokeDelay = 1000, // how frequently we check for clock skew
+    taskHead,
+    taskTail,
+    clockLast = 0,
+    clockNow = 0,
+    clockSkew = 0,
+    clock = typeof performance === "object" && performance.now ? performance : Date,
+    setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };
+
+function now() {
+  return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);
+}
+
+function clearNow() {
+  clockNow = 0;
+}
+
+function Timer() {
+  this._call =
+  this._time =
+  this._next = null;
+}
+
+Timer.prototype = timer.prototype = {
+  constructor: Timer,
+  restart: function(callback, delay, time) {
+    if (typeof callback !== "function") throw new TypeError("callback is not a function");
+    time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);
+    if (!this._next && taskTail !== this) {
+      if (taskTail) taskTail._next = this;
+      else taskHead = this;
+      taskTail = this;
+    }
+    this._call = callback;
+    this._time = time;
+    sleep();
+  },
+  stop: function() {
+    if (this._call) {
+      this._call = null;
+      this._time = Infinity;
+      sleep();
+    }
+  }
+};
+
+function timer(callback, delay, time) {
+  var t = new Timer;
+  t.restart(callback, delay, time);
+  return t;
+}
+
+function timerFlush() {
+  now(); // Get the current time, if not already set.
+  ++frame; // Pretend we’ve set an alarm, if we haven’t already.
+  var t = taskHead, e;
+  while (t) {
+    if ((e = clockNow - t._time) >= 0) t._call.call(null, e);
+    t = t._next;
+  }
+  --frame;
+}
+
+function wake() {
+  clockNow = (clockLast = clock.now()) + clockSkew;
+  frame = timeout = 0;
+  try {
+    timerFlush();
+  } finally {
+    frame = 0;
+    nap();
+    clockNow = 0;
+  }
+}
+
+function poke() {
+  var now = clock.now(), delay = now - clockLast;
+  if (delay > pokeDelay) clockSkew -= delay, clockLast = now;
+}
+
+function nap() {
+  var t0, t1 = taskHead, t2, time = Infinity;
+  while (t1) {
+    if (t1._call) {
+      if (time > t1._time) time = t1._time;
+      t0 = t1, t1 = t1._next;
+    } else {
+      t2 = t1._next, t1._next = null;
+      t1 = t0 ? t0._next = t2 : taskHead = t2;
+    }
+  }
+  taskTail = t0;
+  sleep(time);
+}
+
+function sleep(time) {
+  if (frame) return; // Soonest alarm already set, or will be.
+  if (timeout) timeout = clearTimeout(timeout);
+  var delay = time - clockNow; // Strictly less than if we recomputed clockNow.
+  if (delay > 24) {
+    if (time < Infinity) timeout = setTimeout(wake, time - clock.now() - clockSkew);
+    if (interval) interval = clearInterval(interval);
+  } else {
+    if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay);
+    frame = 1, setFrame(wake);
+  }
+}
+
+function timeout$1(callback, delay, time) {
+  var t = new Timer;
+  delay = delay == null ? 0 : +delay;
+  t.restart(function(elapsed) {
+    t.stop();
+    callback(elapsed + delay);
+  }, delay, time);
+  return t;
+}
+
+function interval$1(callback, delay, time) {
+  var t = new Timer, total = delay;
+  if (delay == null) return t.restart(callback, delay, time), t;
+  delay = +delay, time = time == null ? now() : +time;
+  t.restart(function tick(elapsed) {
+    elapsed += total;
+    t.restart(tick, total += delay, time);
+    callback(elapsed);
+  }, delay, time);
+  return t;
+}
+
+var emptyOn = dispatch("start", "end", "cancel", "interrupt");
+var emptyTween = [];
+
+var CREATED = 0;
+var SCHEDULED = 1;
+var STARTING = 2;
+var STARTED = 3;
+var RUNNING = 4;
+var ENDING = 5;
+var ENDED = 6;
+
+function schedule(node, name, id, index, group, timing) {
+  var schedules = node.__transition;
+  if (!schedules) node.__transition = {};
+  else if (id in schedules) return;
+  create$1(node, id, {
+    name: name,
+    index: index, // For context during callback.
+    group: group, // For context during callback.
+    on: emptyOn,
+    tween: emptyTween,
+    time: timing.time,
+    delay: timing.delay,
+    duration: timing.duration,
+    ease: timing.ease,
+    timer: null,
+    state: CREATED
+  });
+}
+
+function init(node, id) {
+  var schedule = get$1(node, id);
+  if (schedule.state > CREATED) throw new Error("too late; already scheduled");
+  return schedule;
+}
+
+function set$1(node, id) {
+  var schedule = get$1(node, id);
+  if (schedule.state > STARTED) throw new Error("too late; already running");
+  return schedule;
+}
+
+function get$1(node, id) {
+  var schedule = node.__transition;
+  if (!schedule || !(schedule = schedule[id])) throw new Error("transition not found");
+  return schedule;
+}
+
+function create$1(node, id, self) {
+  var schedules = node.__transition,
+      tween;
+
+  // Initialize the self timer when the transition is created.
+  // Note the actual delay is not known until the first callback!
+  schedules[id] = self;
+  self.timer = timer(schedule, 0, self.time);
+
+  function schedule(elapsed) {
+    self.state = SCHEDULED;
+    self.timer.restart(start, self.delay, self.time);
+
+    // If the elapsed delay is less than our first sleep, start immediately.
+    if (self.delay <= elapsed) start(elapsed - self.delay);
+  }
+
+  function start(elapsed) {
+    var i, j, n, o;
+
+    // If the state is not SCHEDULED, then we previously errored on start.
+    if (self.state !== SCHEDULED) return stop();
+
+    for (i in schedules) {
+      o = schedules[i];
+      if (o.name !== self.name) continue;
+
+      // While this element already has a starting transition during this frame,
+      // defer starting an interrupting transition until that transition has a
+      // chance to tick (and possibly end); see d3/d3-transition#54!
+      if (o.state === STARTED) return timeout$1(start);
+
+      // Interrupt the active transition, if any.
+      if (o.state === RUNNING) {
+        o.state = ENDED;
+        o.timer.stop();
+        o.on.call("interrupt", node, node.__data__, o.index, o.group);
+        delete schedules[i];
+      }
+
+      // Cancel any pre-empted transitions.
+      else if (+i < id) {
+        o.state = ENDED;
+        o.timer.stop();
+        o.on.call("cancel", node, node.__data__, o.index, o.group);
+        delete schedules[i];
+      }
+    }
+
+    // Defer the first tick to end of the current frame; see d3/d3#1576.
+    // Note the transition may be canceled after start and before the first tick!
+    // Note this must be scheduled before the start event; see d3/d3-transition#16!
+    // Assuming this is successful, subsequent callbacks go straight to tick.
+    timeout$1(function() {
+      if (self.state === STARTED) {
+        self.state = RUNNING;
+        self.timer.restart(tick, self.delay, self.time);
+        tick(elapsed);
+      }
+    });
+
+    // Dispatch the start event.
+    // Note this must be done before the tween are initialized.
+    self.state = STARTING;
+    self.on.call("start", node, node.__data__, self.index, self.group);
+    if (self.state !== STARTING) return; // interrupted
+    self.state = STARTED;
+
+    // Initialize the tween, deleting null tween.
+    tween = new Array(n = self.tween.length);
+    for (i = 0, j = -1; i < n; ++i) {
+      if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) {
+        tween[++j] = o;
+      }
+    }
+    tween.length = j + 1;
+  }
+
+  function tick(elapsed) {
+    var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),
+        i = -1,
+        n = tween.length;
+
+    while (++i < n) {
+      tween[i].call(node, t);
+    }
+
+    // Dispatch the end event.
+    if (self.state === ENDING) {
+      self.on.call("end", node, node.__data__, self.index, self.group);
+      stop();
+    }
+  }
+
+  function stop() {
+    self.state = ENDED;
+    self.timer.stop();
+    delete schedules[id];
+    for (var i in schedules) return; // eslint-disable-line no-unused-vars
+    delete node.__transition;
+  }
+}
+
+function interrupt(node, name) {
+  var schedules = node.__transition,
+      schedule$$1,
+      active,
+      empty = true,
+      i;
+
+  if (!schedules) return;
+
+  name = name == null ? null : name + "";
+
+  for (i in schedules) {
+    if ((schedule$$1 = schedules[i]).name !== name) { empty = false; continue; }
+    active = schedule$$1.state > STARTING && schedule$$1.state < ENDING;
+    schedule$$1.state = ENDED;
+    schedule$$1.timer.stop();
+    schedule$$1.on.call(active ? "interrupt" : "cancel", node, node.__data__, schedule$$1.index, schedule$$1.group);
+    delete schedules[i];
+  }
+
+  if (empty) delete node.__transition;
+}
+
+function selection_interrupt(name) {
+  return this.each(function() {
+    interrupt(this, name);
+  });
+}
+
+function tweenRemove(id, name) {
+  var tween0, tween1;
+  return function() {
+    var schedule$$1 = set$1(this, id),
+        tween = schedule$$1.tween;
+
+    // If this node shared tween with the previous node,
+    // just assign the updated shared tween and we’re done!
+    // Otherwise, copy-on-write.
+    if (tween !== tween0) {
+      tween1 = tween0 = tween;
+      for (var i = 0, n = tween1.length; i < n; ++i) {
+        if (tween1[i].name === name) {
+          tween1 = tween1.slice();
+          tween1.splice(i, 1);
+          break;
+        }
+      }
+    }
+
+    schedule$$1.tween = tween1;
+  };
+}
+
+function tweenFunction(id, name, value) {
+  var tween0, tween1;
+  if (typeof value !== "function") throw new Error;
+  return function() {
+    var schedule$$1 = set$1(this, id),
+        tween = schedule$$1.tween;
+
+    // If this node shared tween with the previous node,
+    // just assign the updated shared tween and we’re done!
+    // Otherwise, copy-on-write.
+    if (tween !== tween0) {
+      tween1 = (tween0 = tween).slice();
+      for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) {
+        if (tween1[i].name === name) {
+          tween1[i] = t;
+          break;
+        }
+      }
+      if (i === n) tween1.push(t);
+    }
+
+    schedule$$1.tween = tween1;
+  };
+}
+
+function transition_tween(name, value) {
+  var id = this._id;
+
+  name += "";
+
+  if (arguments.length < 2) {
+    var tween = get$1(this.node(), id).tween;
+    for (var i = 0, n = tween.length, t; i < n; ++i) {
+      if ((t = tween[i]).name === name) {
+        return t.value;
+      }
+    }
+    return null;
+  }
+
+  return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value));
+}
+
+function tweenValue(transition, name, value) {
+  var id = transition._id;
+
+  transition.each(function() {
+    var schedule$$1 = set$1(this, id);
+    (schedule$$1.value || (schedule$$1.value = {}))[name] = value.apply(this, arguments);
+  });
+
+  return function(node) {
+    return get$1(node, id).value[name];
+  };
+}
+
+function interpolate(a, b) {
+  var c;
+  return (typeof b === "number" ? interpolateNumber
+      : b instanceof color ? interpolateRgb
+      : (c = color(b)) ? (b = c, interpolateRgb)
+      : interpolateString)(a, b);
+}
+
+function attrRemove$1(name) {
+  return function() {
+    this.removeAttribute(name);
+  };
+}
+
+function attrRemoveNS$1(fullname) {
+  return function() {
+    this.removeAttributeNS(fullname.space, fullname.local);
+  };
+}
+
+function attrConstant$1(name, interpolate$$1, value1) {
+  var string00,
+      string1 = value1 + "",
+      interpolate0;
+  return function() {
+    var string0 = this.getAttribute(name);
+    return string0 === string1 ? null
+        : string0 === string00 ? interpolate0
+        : interpolate0 = interpolate$$1(string00 = string0, value1);
+  };
+}
+
+function attrConstantNS$1(fullname, interpolate$$1, value1) {
+  var string00,
+      string1 = value1 + "",
+      interpolate0;
+  return function() {
+    var string0 = this.getAttributeNS(fullname.space, fullname.local);
+    return string0 === string1 ? null
+        : string0 === string00 ? interpolate0
+        : interpolate0 = interpolate$$1(string00 = string0, value1);
+  };
+}
+
+function attrFunction$1(name, interpolate$$1, value) {
+  var string00,
+      string10,
+      interpolate0;
+  return function() {
+    var string0, value1 = value(this), string1;
+    if (value1 == null) return void this.removeAttribute(name);
+    string0 = this.getAttribute(name);
+    string1 = value1 + "";
+    return string0 === string1 ? null
+        : string0 === string00 && string1 === string10 ? interpolate0
+        : (string10 = string1, interpolate0 = interpolate$$1(string00 = string0, value1));
+  };
+}
+
+function attrFunctionNS$1(fullname, interpolate$$1, value) {
+  var string00,
+      string10,
+      interpolate0;
+  return function() {
+    var string0, value1 = value(this), string1;
+    if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
+    string0 = this.getAttributeNS(fullname.space, fullname.local);
+    string1 = value1 + "";
+    return string0 === string1 ? null
+        : string0 === string00 && string1 === string10 ? interpolate0
+        : (string10 = string1, interpolate0 = interpolate$$1(string00 = string0, value1));
+  };
+}
+
+function transition_attr(name, value) {
+  var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate;
+  return this.attrTween(name, typeof value === "function"
+      ? (fullname.local ? attrFunctionNS$1 : attrFunction$1)(fullname, i, tweenValue(this, "attr." + name, value))
+      : value == null ? (fullname.local ? attrRemoveNS$1 : attrRemove$1)(fullname)
+      : (fullname.local ? attrConstantNS$1 : attrConstant$1)(fullname, i, value));
+}
+
+function attrInterpolate(name, i) {
+  return function(t) {
+    this.setAttribute(name, i(t));
+  };
+}
+
+function attrInterpolateNS(fullname, i) {
+  return function(t) {
+    this.setAttributeNS(fullname.space, fullname.local, i(t));
+  };
+}
+
+function attrTweenNS(fullname, value) {
+  var t0, i0;
+  function tween() {
+    var i = value.apply(this, arguments);
+    if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);
+    return t0;
+  }
+  tween._value = value;
+  return tween;
+}
+
+function attrTween(name, value) {
+  var t0, i0;
+  function tween() {
+    var i = value.apply(this, arguments);
+    if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);
+    return t0;
+  }
+  tween._value = value;
+  return tween;
+}
+
+function transition_attrTween(name, value) {
+  var key = "attr." + name;
+  if (arguments.length < 2) return (key = this.tween(key)) && key._value;
+  if (value == null) return this.tween(key, null);
+  if (typeof value !== "function") throw new Error;
+  var fullname = namespace(name);
+  return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value));
+}
+
+function delayFunction(id, value) {
+  return function() {
+    init(this, id).delay = +value.apply(this, arguments);
+  };
+}
+
+function delayConstant(id, value) {
+  return value = +value, function() {
+    init(this, id).delay = value;
+  };
+}
+
+function transition_delay(value) {
+  var id = this._id;
+
+  return arguments.length
+      ? this.each((typeof value === "function"
+          ? delayFunction
+          : delayConstant)(id, value))
+      : get$1(this.node(), id).delay;
+}
+
+function durationFunction(id, value) {
+  return function() {
+    set$1(this, id).duration = +value.apply(this, arguments);
+  };
+}
+
+function durationConstant(id, value) {
+  return value = +value, function() {
+    set$1(this, id).duration = value;
+  };
+}
+
+function transition_duration(value) {
+  var id = this._id;
+
+  return arguments.length
+      ? this.each((typeof value === "function"
+          ? durationFunction
+          : durationConstant)(id, value))
+      : get$1(this.node(), id).duration;
+}
+
+function easeConstant(id, value) {
+  if (typeof value !== "function") throw new Error;
+  return function() {
+    set$1(this, id).ease = value;
+  };
+}
+
+function transition_ease(value) {
+  var id = this._id;
+
+  return arguments.length
+      ? this.each(easeConstant(id, value))
+      : get$1(this.node(), id).ease;
+}
+
+function transition_filter(match) {
+  if (typeof match !== "function") match = matcher(match);
+
+  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
+      if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
+        subgroup.push(node);
+      }
+    }
+  }
+
+  return new Transition(subgroups, this._parents, this._name, this._id);
+}
+
+function transition_merge(transition$$1) {
+  if (transition$$1._id !== this._id) throw new Error;
+
+  for (var groups0 = this._groups, groups1 = transition$$1._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
+    for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
+      if (node = group0[i] || group1[i]) {
+        merge[i] = node;
+      }
+    }
+  }
+
+  for (; j < m0; ++j) {
+    merges[j] = groups0[j];
+  }
+
+  return new Transition(merges, this._parents, this._name, this._id);
+}
+
+function start(name) {
+  return (name + "").trim().split(/^|\s+/).every(function(t) {
+    var i = t.indexOf(".");
+    if (i >= 0) t = t.slice(0, i);
+    return !t || t === "start";
+  });
+}
+
+function onFunction(id, name, listener) {
+  var on0, on1, sit = start(name) ? init : set$1;
+  return function() {
+    var schedule$$1 = sit(this, id),
+        on = schedule$$1.on;
+
+    // If this node shared a dispatch with the previous node,
+    // just assign the updated shared dispatch and we’re done!
+    // Otherwise, copy-on-write.
+    if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener);
+
+    schedule$$1.on = on1;
+  };
+}
+
+function transition_on(name, listener) {
+  var id = this._id;
+
+  return arguments.length < 2
+      ? get$1(this.node(), id).on.on(name)
+      : this.each(onFunction(id, name, listener));
+}
+
+function removeFunction(id) {
+  return function() {
+    var parent = this.parentNode;
+    for (var i in this.__transition) if (+i !== id) return;
+    if (parent) parent.removeChild(this);
+  };
+}
+
+function transition_remove() {
+  return this.on("end.remove", removeFunction(this._id));
+}
+
+function transition_select(select$$1) {
+  var name = this._name,
+      id = this._id;
+
+  if (typeof select$$1 !== "function") select$$1 = selector(select$$1);
+
+  for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
+      if ((node = group[i]) && (subnode = select$$1.call(node, node.__data__, i, group))) {
+        if ("__data__" in node) subnode.__data__ = node.__data__;
+        subgroup[i] = subnode;
+        schedule(subgroup[i], name, id, i, subgroup, get$1(node, id));
+      }
+    }
+  }
+
+  return new Transition(subgroups, this._parents, name, id);
+}
+
+function transition_selectAll(select$$1) {
+  var name = this._name,
+      id = this._id;
+
+  if (typeof select$$1 !== "function") select$$1 = selectorAll(select$$1);
+
+  for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+      if (node = group[i]) {
+        for (var children = select$$1.call(node, node.__data__, i, group), child, inherit = get$1(node, id), k = 0, l = children.length; k < l; ++k) {
+          if (child = children[k]) {
+            schedule(child, name, id, k, children, inherit);
+          }
+        }
+        subgroups.push(children);
+        parents.push(node);
+      }
+    }
+  }
+
+  return new Transition(subgroups, parents, name, id);
+}
+
+var Selection$1 = selection.prototype.constructor;
+
+function transition_selection() {
+  return new Selection$1(this._groups, this._parents);
+}
+
+function styleNull(name, interpolate$$1) {
+  var string00,
+      string10,
+      interpolate0;
+  return function() {
+    var string0 = styleValue(this, name),
+        string1 = (this.style.removeProperty(name), styleValue(this, name));
+    return string0 === string1 ? null
+        : string0 === string00 && string1 === string10 ? interpolate0
+        : interpolate0 = interpolate$$1(string00 = string0, string10 = string1);
+  };
+}
+
+function styleRemove$1(name) {
+  return function() {
+    this.style.removeProperty(name);
+  };
+}
+
+function styleConstant$1(name, interpolate$$1, value1) {
+  var string00,
+      string1 = value1 + "",
+      interpolate0;
+  return function() {
+    var string0 = styleValue(this, name);
+    return string0 === string1 ? null
+        : string0 === string00 ? interpolate0
+        : interpolate0 = interpolate$$1(string00 = string0, value1);
+  };
+}
+
+function styleFunction$1(name, interpolate$$1, value) {
+  var string00,
+      string10,
+      interpolate0;
+  return function() {
+    var string0 = styleValue(this, name),
+        value1 = value(this),
+        string1 = value1 + "";
+    if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
+    return string0 === string1 ? null
+        : string0 === string00 && string1 === string10 ? interpolate0
+        : (string10 = string1, interpolate0 = interpolate$$1(string00 = string0, value1));
+  };
+}
+
+function styleMaybeRemove(id, name) {
+  var on0, on1, listener0, key = "style." + name, event = "end." + key, remove;
+  return function() {
+    var schedule$$1 = set$1(this, id),
+        on = schedule$$1.on,
+        listener = schedule$$1.value[key] == null ? remove || (remove = styleRemove$1(name)) : undefined;
+
+    // If this node shared a dispatch with the previous node,
+    // just assign the updated shared dispatch and we’re done!
+    // Otherwise, copy-on-write.
+    if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);
+
+    schedule$$1.on = on1;
+  };
+}
+
+function transition_style(name, value, priority) {
+  var i = (name += "") === "transform" ? interpolateTransformCss : interpolate;
+  return value == null ? this
+      .styleTween(name, styleNull(name, i))
+      .on("end.style." + name, styleRemove$1(name))
+    : typeof value === "function" ? this
+      .styleTween(name, styleFunction$1(name, i, tweenValue(this, "style." + name, value)))
+      .each(styleMaybeRemove(this._id, name))
+    : this
+      .styleTween(name, styleConstant$1(name, i, value), priority)
+      .on("end.style." + name, null);
+}
+
+function styleInterpolate(name, i, priority) {
+  return function(t) {
+    this.style.setProperty(name, i(t), priority);
+  };
+}
+
+function styleTween(name, value, priority) {
+  var t, i0;
+  function tween() {
+    var i = value.apply(this, arguments);
+    if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);
+    return t;
+  }
+  tween._value = value;
+  return tween;
+}
+
+function transition_styleTween(name, value, priority) {
+  var key = "style." + (name += "");
+  if (arguments.length < 2) return (key = this.tween(key)) && key._value;
+  if (value == null) return this.tween(key, null);
+  if (typeof value !== "function") throw new Error;
+  return this.tween(key, styleTween(name, value, priority == null ? "" : priority));
+}
+
+function textConstant$1(value) {
+  return function() {
+    this.textContent = value;
+  };
+}
+
+function textFunction$1(value) {
+  return function() {
+    var value1 = value(this);
+    this.textContent = value1 == null ? "" : value1;
+  };
+}
+
+function transition_text(value) {
+  return this.tween("text", typeof value === "function"
+      ? textFunction$1(tweenValue(this, "text", value))
+      : textConstant$1(value == null ? "" : value + ""));
+}
+
+function transition_transition() {
+  var name = this._name,
+      id0 = this._id,
+      id1 = newId();
+
+  for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+      if (node = group[i]) {
+        var inherit = get$1(node, id0);
+        schedule(node, name, id1, i, group, {
+          time: inherit.time + inherit.delay + inherit.duration,
+          delay: 0,
+          duration: inherit.duration,
+          ease: inherit.ease
+        });
+      }
+    }
+  }
+
+  return new Transition(groups, this._parents, name, id1);
+}
+
+function transition_end() {
+  var on0, on1, that = this, id = that._id, size = that.size();
+  return new Promise(function(resolve, reject) {
+    var cancel = {value: reject},
+        end = {value: function() { if (--size === 0) resolve(); }};
+
+    that.each(function() {
+      var schedule$$1 = set$1(this, id),
+          on = schedule$$1.on;
+
+      // If this node shared a dispatch with the previous node,
+      // just assign the updated shared dispatch and we’re done!
+      // Otherwise, copy-on-write.
+      if (on !== on0) {
+        on1 = (on0 = on).copy();
+        on1._.cancel.push(cancel);
+        on1._.interrupt.push(cancel);
+        on1._.end.push(end);
+      }
+
+      schedule$$1.on = on1;
+    });
+  });
+}
+
+var id = 0;
+
+function Transition(groups, parents, name, id) {
+  this._groups = groups;
+  this._parents = parents;
+  this._name = name;
+  this._id = id;
+}
+
+function transition(name) {
+  return selection().transition(name);
+}
+
+function newId() {
+  return ++id;
+}
+
+var selection_prototype = selection.prototype;
+
+Transition.prototype = transition.prototype = {
+  constructor: Transition,
+  select: transition_select,
+  selectAll: transition_selectAll,
+  filter: transition_filter,
+  merge: transition_merge,
+  selection: transition_selection,
+  transition: transition_transition,
+  call: selection_prototype.call,
+  nodes: selection_prototype.nodes,
+  node: selection_prototype.node,
+  size: selection_prototype.size,
+  empty: selection_prototype.empty,
+  each: selection_prototype.each,
+  on: transition_on,
+  attr: transition_attr,
+  attrTween: transition_attrTween,
+  style: transition_style,
+  styleTween: transition_styleTween,
+  text: transition_text,
+  remove: transition_remove,
+  tween: transition_tween,
+  delay: transition_delay,
+  duration: transition_duration,
+  ease: transition_ease,
+  end: transition_end
+};
+
+function linear$1(t) {
+  return +t;
+}
+
+function quadIn(t) {
+  return t * t;
+}
+
+function quadOut(t) {
+  return t * (2 - t);
+}
+
+function quadInOut(t) {
+  return ((t *= 2) <= 1 ? t * t : --t * (2 - t) + 1) / 2;
+}
+
+function cubicIn(t) {
+  return t * t * t;
+}
+
+function cubicOut(t) {
+  return --t * t * t + 1;
+}
+
+function cubicInOut(t) {
+  return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
+}
+
+var exponent = 3;
+
+var polyIn = (function custom(e) {
+  e = +e;
+
+  function polyIn(t) {
+    return Math.pow(t, e);
+  }
+
+  polyIn.exponent = custom;
+
+  return polyIn;
+})(exponent);
+
+var polyOut = (function custom(e) {
+  e = +e;
+
+  function polyOut(t) {
+    return 1 - Math.pow(1 - t, e);
+  }
+
+  polyOut.exponent = custom;
+
+  return polyOut;
+})(exponent);
+
+var polyInOut = (function custom(e) {
+  e = +e;
+
+  function polyInOut(t) {
+    return ((t *= 2) <= 1 ? Math.pow(t, e) : 2 - Math.pow(2 - t, e)) / 2;
+  }
+
+  polyInOut.exponent = custom;
+
+  return polyInOut;
+})(exponent);
+
+var pi = Math.PI,
+    halfPi = pi / 2;
+
+function sinIn(t) {
+  return 1 - Math.cos(t * halfPi);
+}
+
+function sinOut(t) {
+  return Math.sin(t * halfPi);
+}
+
+function sinInOut(t) {
+  return (1 - Math.cos(pi * t)) / 2;
+}
+
+function expIn(t) {
+  return Math.pow(2, 10 * t - 10);
+}
+
+function expOut(t) {
+  return 1 - Math.pow(2, -10 * t);
+}
+
+function expInOut(t) {
+  return ((t *= 2) <= 1 ? Math.pow(2, 10 * t - 10) : 2 - Math.pow(2, 10 - 10 * t)) / 2;
+}
+
+function circleIn(t) {
+  return 1 - Math.sqrt(1 - t * t);
+}
+
+function circleOut(t) {
+  return Math.sqrt(1 - --t * t);
+}
+
+function circleInOut(t) {
+  return ((t *= 2) <= 1 ? 1 - Math.sqrt(1 - t * t) : Math.sqrt(1 - (t -= 2) * t) + 1) / 2;
+}
+
+var b1 = 4 / 11,
+    b2 = 6 / 11,
+    b3 = 8 / 11,
+    b4 = 3 / 4,
+    b5 = 9 / 11,
+    b6 = 10 / 11,
+    b7 = 15 / 16,
+    b8 = 21 / 22,
+    b9 = 63 / 64,
+    b0 = 1 / b1 / b1;
+
+function bounceIn(t) {
+  return 1 - bounceOut(1 - t);
+}
+
+function bounceOut(t) {
+  return (t = +t) < b1 ? b0 * t * t : t < b3 ? b0 * (t -= b2) * t + b4 : t < b6 ? b0 * (t -= b5) * t + b7 : b0 * (t -= b8) * t + b9;
+}
+
+function bounceInOut(t) {
+  return ((t *= 2) <= 1 ? 1 - bounceOut(1 - t) : bounceOut(t - 1) + 1) / 2;
+}
+
+var overshoot = 1.70158;
+
+var backIn = (function custom(s) {
+  s = +s;
+
+  function backIn(t) {
+    return t * t * ((s + 1) * t - s);
+  }
+
+  backIn.overshoot = custom;
+
+  return backIn;
+})(overshoot);
+
+var backOut = (function custom(s) {
+  s = +s;
+
+  function backOut(t) {
+    return --t * t * ((s + 1) * t + s) + 1;
+  }
+
+  backOut.overshoot = custom;
+
+  return backOut;
+})(overshoot);
+
+var backInOut = (function custom(s) {
+  s = +s;
+
+  function backInOut(t) {
+    return ((t *= 2) < 1 ? t * t * ((s + 1) * t - s) : (t -= 2) * t * ((s + 1) * t + s) + 2) / 2;
+  }
+
+  backInOut.overshoot = custom;
+
+  return backInOut;
+})(overshoot);
+
+var tau = 2 * Math.PI,
+    amplitude = 1,
+    period = 0.3;
+
+var elasticIn = (function custom(a, p) {
+  var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);
+
+  function elasticIn(t) {
+    return a * Math.pow(2, 10 * --t) * Math.sin((s - t) / p);
+  }
+
+  elasticIn.amplitude = function(a) { return custom(a, p * tau); };
+  elasticIn.period = function(p) { return custom(a, p); };
+
+  return elasticIn;
+})(amplitude, period);
+
+var elasticOut = (function custom(a, p) {
+  var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);
+
+  function elasticOut(t) {
+    return 1 - a * Math.pow(2, -10 * (t = +t)) * Math.sin((t + s) / p);
+  }
+
+  elasticOut.amplitude = function(a) { return custom(a, p * tau); };
+  elasticOut.period = function(p) { return custom(a, p); };
+
+  return elasticOut;
+})(amplitude, period);
+
+var elasticInOut = (function custom(a, p) {
+  var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);
+
+  function elasticInOut(t) {
+    return ((t = t * 2 - 1) < 0
+        ? a * Math.pow(2, 10 * t) * Math.sin((s - t) / p)
+        : 2 - a * Math.pow(2, -10 * t) * Math.sin((s + t) / p)) / 2;
+  }
+
+  elasticInOut.amplitude = function(a) { return custom(a, p * tau); };
+  elasticInOut.period = function(p) { return custom(a, p); };
+
+  return elasticInOut;
+})(amplitude, period);
+
+var defaultTiming = {
+  time: null, // Set on use.
+  delay: 0,
+  duration: 250,
+  ease: cubicInOut
+};
+
+function inherit(node, id) {
+  var timing;
+  while (!(timing = node.__transition) || !(timing = timing[id])) {
+    if (!(node = node.parentNode)) {
+      return defaultTiming.time = now(), defaultTiming;
+    }
+  }
+  return timing;
+}
+
+function selection_transition(name) {
+  var id,
+      timing;
+
+  if (name instanceof Transition) {
+    id = name._id, name = name._name;
+  } else {
+    id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + "";
+  }
+
+  for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
+    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+      if (node = group[i]) {
+        schedule(node, name, id, i, group, timing || inherit(node, id));
+      }
+    }
+  }
+
+  return new Transition(groups, this._parents, name, id);
+}
+
+selection.prototype.interrupt = selection_interrupt;
+selection.prototype.transition = selection_transition;
+
+var root$1 = [null];
+
+function active(node, name) {
+  var schedules = node.__transition,
+      schedule$$1,
+      i;
+
+  if (schedules) {
+    name = name == null ? null : name + "";
+    for (i in schedules) {
+      if ((schedule$$1 = schedules[i]).state > SCHEDULED && schedule$$1.name === name) {
+        return new Transition([[node]], root$1, name, +i);
+      }
+    }
+  }
+
+  return null;
+}
+
+function constant$4(x) {
+  return function() {
+    return x;
+  };
+}
+
+function BrushEvent(target, type, selection) {
+  this.target = target;
+  this.type = type;
+  this.selection = selection;
+}
+
+function nopropagation$1() {
+  exports.event.stopImmediatePropagation();
+}
+
+function noevent$1() {
+  exports.event.preventDefault();
+  exports.event.stopImmediatePropagation();
+}
+
+var MODE_DRAG = {name: "drag"},
+    MODE_SPACE = {name: "space"},
+    MODE_HANDLE = {name: "handle"},
+    MODE_CENTER = {name: "center"};
+
+var X = {
+  name: "x",
+  handles: ["e", "w"].map(type),
+  input: function(x, e) { return x && [[x[0], e[0][1]], [x[1], e[1][1]]]; },
+  output: function(xy) { return xy && [xy[0][0], xy[1][0]]; }
+};
+
+var Y = {
+  name: "y",
+  handles: ["n", "s"].map(type),
+  input: function(y, e) { return y && [[e[0][0], y[0]], [e[1][0], y[1]]]; },
+  output: function(xy) { return xy && [xy[0][1], xy[1][1]]; }
+};
+
+var XY = {
+  name: "xy",
+  handles: ["n", "e", "s", "w", "nw", "ne", "se", "sw"].map(type),
+  input: function(xy) { return xy; },
+  output: function(xy) { return xy; }
+};
+
+var cursors = {
+  overlay: "crosshair",
+  selection: "move",
+  n: "ns-resize",
+  e: "ew-resize",
+  s: "ns-resize",
+  w: "ew-resize",
+  nw: "nwse-resize",
+  ne: "nesw-resize",
+  se: "nwse-resize",
+  sw: "nesw-resize"
+};
+
+var flipX = {
+  e: "w",
+  w: "e",
+  nw: "ne",
+  ne: "nw",
+  se: "sw",
+  sw: "se"
+};
+
+var flipY = {
+  n: "s",
+  s: "n",
+  nw: "sw",
+  ne: "se",
+  se: "ne",
+  sw: "nw"
+};
+
+var signsX = {
+  overlay: +1,
+  selection: +1,
+  n: null,
+  e: +1,
+  s: null,
+  w: -1,
+  nw: -1,
+  ne: +1,
+  se: +1,
+  sw: -1
+};
+
+var signsY = {
+  overlay: +1,
+  selection: +1,
+  n: -1,
+  e: null,
+  s: +1,
+  w: null,
+  nw: -1,
+  ne: -1,
+  se: +1,
+  sw: +1
+};
+
+function type(t) {
+  return {type: t};
+}
+
+// Ignore right-click, since that should open the context menu.
+function defaultFilter$1() {
+  return !exports.event.button;
+}
+
+function defaultExtent() {
+  var svg = this.ownerSVGElement || this;
+  return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]];
+}
+
+// Like d3.local, but with the name “__brush” rather than auto-generated.
+function local$1(node) {
+  while (!node.__brush) if (!(node = node.parentNode)) return;
+  return node.__brush;
+}
+
+function empty$1(extent) {
+  return extent[0][0] === extent[1][0]
+      || extent[0][1] === extent[1][1];
+}
+
+function brushSelection(node) {
+  var state = node.__brush;
+  return state ? state.dim.output(state.selection) : null;
+}
+
+function brushX() {
+  return brush$1(X);
+}
+
+function brushY() {
+  return brush$1(Y);
+}
+
+function brush() {
+  return brush$1(XY);
+}
+
+function brush$1(dim) {
+  var extent = defaultExtent,
+      filter = defaultFilter$1,
+      listeners = dispatch(brush, "start", "brush", "end"),
+      handleSize = 6,
+      touchending;
+
+  function brush(group) {
+    var overlay = group
+        .property("__brush", initialize)
+      .selectAll(".overlay")
+      .data([type("overlay")]);
+
+    overlay.enter().append("rect")
+        .attr("class", "overlay")
+        .attr("pointer-events", "all")
+        .attr("cursor", cursors.overlay)
+      .merge(overlay)
+        .each(function() {
+          var extent = local$1(this).extent;
+          select(this)
+              .attr("x", extent[0][0])
+              .attr("y", extent[0][1])
+              .attr("width", extent[1][0] - extent[0][0])
+              .attr("height", extent[1][1] - extent[0][1]);
+        });
+
+    group.selectAll(".selection")
+      .data([type("selection")])
+      .enter().append("rect")
+        .attr("class", "selection")
+        .attr("cursor", cursors.selection)
+        .attr("fill", "#777")
+        .attr("fill-opacity", 0.3)
+        .attr("stroke", "#fff")
+        .attr("shape-rendering", "crispEdges");
+
+    var handle = group.selectAll(".handle")
+      .data(dim.handles, function(d) { return d.type; });
+
+    handle.exit().remove();
+
+    handle.enter().append("rect")
+        .attr("class", function(d) { return "handle handle--" + d.type; })
+        .attr("cursor", function(d) { return cursors[d.type]; });
+
+    group
+        .each(redraw)
+        .attr("fill", "none")
+        .attr("pointer-events", "all")
+        .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)")
+        .on("mousedown.brush touchstart.brush", started);
+  }
+
+  brush.move = function(group, selection$$1) {
+    if (group.selection) {
+      group
+          .on("start.brush", function() { emitter(this, arguments).beforestart().start(); })
+          .on("interrupt.brush end.brush", function() { emitter(this, arguments).end(); })
+          .tween("brush", function() {
+            var that = this,
+                state = that.__brush,
+                emit = emitter(that, arguments),
+                selection0 = state.selection,
+                selection1 = dim.input(typeof selection$$1 === "function" ? selection$$1.apply(this, arguments) : selection$$1, state.extent),
+                i = interpolateValue(selection0, selection1);
+
+            function tween(t) {
+              state.selection = t === 1 && empty$1(selection1) ? null : i(t);
+              redraw.call(that);
+              emit.brush();
+            }
+
+            return selection0 && selection1 ? tween : tween(1);
+          });
+    } else {
+      group
+          .each(function() {
+            var that = this,
+                args = arguments,
+                state = that.__brush,
+                selection1 = dim.input(typeof selection$$1 === "function" ? selection$$1.apply(that, args) : selection$$1, state.extent),
+                emit = emitter(that, args).beforestart();
+
+            interrupt(that);
+            state.selection = selection1 == null || empty$1(selection1) ? null : selection1;
+            redraw.call(that);
+            emit.start().brush().end();
+          });
+    }
+  };
+
+  function redraw() {
+    var group = select(this),
+        selection$$1 = local$1(this).selection;
+
+    if (selection$$1) {
+      group.selectAll(".selection")
+          .style("display", null)
+          .attr("x", selection$$1[0][0])
+          .attr("y", selection$$1[0][1])
+          .attr("width", selection$$1[1][0] - selection$$1[0][0])
+          .attr("height", selection$$1[1][1] - selection$$1[0][1]);
+
+      group.selectAll(".handle")
+          .style("display", null)
+          .attr("x", function(d) { return d.type[d.type.length - 1] === "e" ? selection$$1[1][0] - handleSize / 2 : selection$$1[0][0] - handleSize / 2; })
+          .attr("y", function(d) { return d.type[0] === "s" ? selection$$1[1][1] - handleSize / 2 : selection$$1[0][1] - handleSize / 2; })
+          .attr("width", function(d) { return d.type === "n" || d.type === "s" ? selection$$1[1][0] - selection$$1[0][0] + handleSize : handleSize; })
+          .attr("height", function(d) { return d.type === "e" || d.type === "w" ? selection$$1[1][1] - selection$$1[0][1] + handleSize : handleSize; });
+    }
+
+    else {
+      group.selectAll(".selection,.handle")
+          .style("display", "none")
+          .attr("x", null)
+          .attr("y", null)
+          .attr("width", null)
+          .attr("height", null);
+    }
+  }
+
+  function emitter(that, args) {
+    return that.__brush.emitter || new Emitter(that, args);
+  }
+
+  function Emitter(that, args) {
+    this.that = that;
+    this.args = args;
+    this.state = that.__brush;
+    this.active = 0;
+  }
+
+  Emitter.prototype = {
+    beforestart: function() {
+      if (++this.active === 1) this.state.emitter = this, this.starting = true;
+      return this;
+    },
+    start: function() {
+      if (this.starting) this.starting = false, this.emit("start");
+      return this;
+    },
+    brush: function() {
+      this.emit("brush");
+      return this;
+    },
+    end: function() {
+      if (--this.active === 0) delete this.state.emitter, this.emit("end");
+      return this;
+    },
+    emit: function(type) {
+      customEvent(new BrushEvent(brush, type, dim.output(this.state.selection)), listeners.apply, listeners, [type, this.that, this.args]);
+    }
+  };
+
+  function started() {
+    if (exports.event.touches) { if (exports.event.changedTouches.length < exports.event.touches.length) return noevent$1(); }
+    else if (touchending) return;
+    if (!filter.apply(this, arguments)) return;
+
+    var that = this,
+        type = exports.event.target.__data__.type,
+        mode = (exports.event.metaKey ? type = "overlay" : type) === "selection" ? MODE_DRAG : (exports.event.altKey ? MODE_CENTER : MODE_HANDLE),
+        signX = dim === Y ? null : signsX[type],
+        signY = dim === X ? null : signsY[type],
+        state = local$1(that),
+        extent = state.extent,
+        selection$$1 = state.selection,
+        W = extent[0][0], w0, w1,
+        N = extent[0][1], n0, n1,
+        E = extent[1][0], e0, e1,
+        S = extent[1][1], s0, s1,
+        dx,
+        dy,
+        moving,
+        shifting = signX && signY && exports.event.shiftKey,
+        lockX,
+        lockY,
+        point0 = mouse(that),
+        point$$1 = point0,
+        emit = emitter(that, arguments).beforestart();
+
+    if (type === "overlay") {
+      state.selection = selection$$1 = [
+        [w0 = dim === Y ? W : point0[0], n0 = dim === X ? N : point0[1]],
+        [e0 = dim === Y ? E : w0, s0 = dim === X ? S : n0]
+      ];
+    } else {
+      w0 = selection$$1[0][0];
+      n0 = selection$$1[0][1];
+      e0 = selection$$1[1][0];
+      s0 = selection$$1[1][1];
+    }
+
+    w1 = w0;
+    n1 = n0;
+    e1 = e0;
+    s1 = s0;
+
+    var group = select(that)
+        .attr("pointer-events", "none");
+
+    var overlay = group.selectAll(".overlay")
+        .attr("cursor", cursors[type]);
+
+    if (exports.event.touches) {
+      group
+          .on("touchmove.brush", moved, true)
+          .on("touchend.brush touchcancel.brush", ended, true);
+    } else {
+      var view = select(exports.event.view)
+          .on("keydown.brush", keydowned, true)
+          .on("keyup.brush", keyupped, true)
+          .on("mousemove.brush", moved, true)
+          .on("mouseup.brush", ended, true);
+
+      dragDisable(exports.event.view);
+    }
+
+    nopropagation$1();
+    interrupt(that);
+    redraw.call(that);
+    emit.start();
+
+    function moved() {
+      var point1 = mouse(that);
+      if (shifting && !lockX && !lockY) {
+        if (Math.abs(point1[0] - point$$1[0]) > Math.abs(point1[1] - point$$1[1])) lockY = true;
+        else lockX = true;
+      }
+      point$$1 = point1;
+      moving = true;
+      noevent$1();
+      move();
+    }
+
+    function move() {
+      var t;
+
+      dx = point$$1[0] - point0[0];
+      dy = point$$1[1] - point0[1];
+
+      switch (mode) {
+        case MODE_SPACE:
+        case MODE_DRAG: {
+          if (signX) dx = Math.max(W - w0, Math.min(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx;
+          if (signY) dy = Math.max(N - n0, Math.min(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy;
+          break;
+        }
+        case MODE_HANDLE: {
+          if (signX < 0) dx = Math.max(W - w0, Math.min(E - w0, dx)), w1 = w0 + dx, e1 = e0;
+          else if (signX > 0) dx = Math.max(W - e0, Math.min(E - e0, dx)), w1 = w0, e1 = e0 + dx;
+          if (signY < 0) dy = Math.max(N - n0, Math.min(S - n0, dy)), n1 = n0 + dy, s1 = s0;
+          else if (signY > 0) dy = Math.max(N - s0, Math.min(S - s0, dy)), n1 = n0, s1 = s0 + dy;
+          break;
+        }
+        case MODE_CENTER: {
+          if (signX) w1 = Math.max(W, Math.min(E, w0 - dx * signX)), e1 = Math.max(W, Math.min(E, e0 + dx * signX));
+          if (signY) n1 = Math.max(N, Math.min(S, n0 - dy * signY)), s1 = Math.max(N, Math.min(S, s0 + dy * signY));
+          break;
+        }
+      }
+
+      if (e1 < w1) {
+        signX *= -1;
+        t = w0, w0 = e0, e0 = t;
+        t = w1, w1 = e1, e1 = t;
+        if (type in flipX) overlay.attr("cursor", cursors[type = flipX[type]]);
+      }
+
+      if (s1 < n1) {
+        signY *= -1;
+        t = n0, n0 = s0, s0 = t;
+        t = n1, n1 = s1, s1 = t;
+        if (type in flipY) overlay.attr("cursor", cursors[type = flipY[type]]);
+      }
+
+      if (state.selection) selection$$1 = state.selection; // May be set by brush.move!
+      if (lockX) w1 = selection$$1[0][0], e1 = selection$$1[1][0];
+      if (lockY) n1 = selection$$1[0][1], s1 = selection$$1[1][1];
+
+      if (selection$$1[0][0] !== w1
+          || selection$$1[0][1] !== n1
+          || selection$$1[1][0] !== e1
+          || selection$$1[1][1] !== s1) {
+        state.selection = [[w1, n1], [e1, s1]];
+        redraw.call(that);
+        emit.brush();
+      }
+    }
+
+    function ended() {
+      nopropagation$1();
+      if (exports.event.touches) {
+        if (exports.event.touches.length) return;
+        if (touchending) clearTimeout(touchending);
+        touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
+        group.on("touchmove.brush touchend.brush touchcancel.brush", null);
+      } else {
+        yesdrag(exports.event.view, moving);
+        view.on("keydown.brush keyup.brush mousemove.brush mouseup.brush", null);
+      }
+      group.attr("pointer-events", "all");
+      overlay.attr("cursor", cursors.overlay);
+      if (state.selection) selection$$1 = state.selection; // May be set by brush.move (on start)!
+      if (empty$1(selection$$1)) state.selection = null, redraw.call(that);
+      emit.end();
+    }
+
+    function keydowned() {
+      switch (exports.event.keyCode) {
+        case 16: { // SHIFT
+          shifting = signX && signY;
+          break;
+        }
+        case 18: { // ALT
+          if (mode === MODE_HANDLE) {
+            if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
+            if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
+            mode = MODE_CENTER;
+            move();
+          }
+          break;
+        }
+        case 32: { // SPACE; takes priority over ALT
+          if (mode === MODE_HANDLE || mode === MODE_CENTER) {
+            if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx;
+            if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy;
+            mode = MODE_SPACE;
+            overlay.attr("cursor", cursors.selection);
+            move();
+          }
+          break;
+        }
+        default: return;
+      }
+      noevent$1();
+    }
+
+    function keyupped() {
+      switch (exports.event.keyCode) {
+        case 16: { // SHIFT
+          if (shifting) {
+            lockX = lockY = shifting = false;
+            move();
+          }
+          break;
+        }
+        case 18: { // ALT
+          if (mode === MODE_CENTER) {
+            if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
+            if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
+            mode = MODE_HANDLE;
+            move();
+          }
+          break;
+        }
+        case 32: { // SPACE
+          if (mode === MODE_SPACE) {
+            if (exports.event.altKey) {
+              if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
+              if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
+              mode = MODE_CENTER;
+            } else {
+              if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
+              if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
+              mode = MODE_HANDLE;
+            }
+            overlay.attr("cursor", cursors[type]);
+            move();
+          }
+          break;
+        }
+        default: return;
+      }
+      noevent$1();
+    }
+  }
+
+  function initialize() {
+    var state = this.__brush || {selection: null};
+    state.extent = extent.apply(this, arguments);
+    state.dim = dim;
+    return state;
+  }
+
+  brush.extent = function(_) {
+    return arguments.length ? (extent = typeof _ === "function" ? _ : constant$4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), brush) : extent;
+  };
+
+  brush.filter = function(_) {
+    return arguments.length ? (filter = typeof _ === "function" ? _ : constant$4(!!_), brush) : filter;
+  };
+
+  brush.handleSize = function(_) {
+    return arguments.length ? (handleSize = +_, brush) : handleSize;
+  };
+
+  brush.on = function() {
+    var value = listeners.on.apply(listeners, arguments);
+    return value === listeners ? brush : value;
+  };
+
+  return brush;
+}
+
+var cos = Math.cos;
+var sin = Math.sin;
+var pi$1 = Math.PI;
+var halfPi$1 = pi$1 / 2;
+var tau$1 = pi$1 * 2;
+var max$1 = Math.max;
+
+function compareValue(compare) {
+  return function(a, b) {
+    return compare(
+      a.source.value + a.target.value,
+      b.source.value + b.target.value
+    );
+  };
+}
+
+function chord() {
+  var padAngle = 0,
+      sortGroups = null,
+      sortSubgroups = null,
+      sortChords = null;
+
+  function chord(matrix) {
+    var n = matrix.length,
+        groupSums = [],
+        groupIndex = sequence(n),
+        subgroupIndex = [],
+        chords = [],
+        groups = chords.groups = new Array(n),
+        subgroups = new Array(n * n),
+        k,
+        x,
+        x0,
+        dx,
+        i,
+        j;
+
+    // Compute the sum.
+    k = 0, i = -1; while (++i < n) {
+      x = 0, j = -1; while (++j < n) {
+        x += matrix[i][j];
+      }
+      groupSums.push(x);
+      subgroupIndex.push(sequence(n));
+      k += x;
+    }
+
+    // Sort groups…
+    if (sortGroups) groupIndex.sort(function(a, b) {
+      return sortGroups(groupSums[a], groupSums[b]);
+    });
+
+    // Sort subgroups…
+    if (sortSubgroups) subgroupIndex.forEach(function(d, i) {
+      d.sort(function(a, b) {
+        return sortSubgroups(matrix[i][a], matrix[i][b]);
+      });
+    });
+
+    // Convert the sum to scaling factor for [0, 2pi].
+    // TODO Allow start and end angle to be specified?
+    // TODO Allow padding to be specified as percentage?
+    k = max$1(0, tau$1 - padAngle * n) / k;
+    dx = k ? padAngle : tau$1 / n;
+
+    // Compute the start and end angle for each group and subgroup.
+    // Note: Opera has a bug reordering object literal properties!
+    x = 0, i = -1; while (++i < n) {
+      x0 = x, j = -1; while (++j < n) {
+        var di = groupIndex[i],
+            dj = subgroupIndex[di][j],
+            v = matrix[di][dj],
+            a0 = x,
+            a1 = x += v * k;
+        subgroups[dj * n + di] = {
+          index: di,
+          subindex: dj,
+          startAngle: a0,
+          endAngle: a1,
+          value: v
+        };
+      }
+      groups[di] = {
+        index: di,
+        startAngle: x0,
+        endAngle: x,
+        value: groupSums[di]
+      };
+      x += dx;
+    }
+
+    // Generate chords for each (non-empty) subgroup-subgroup link.
+    i = -1; while (++i < n) {
+      j = i - 1; while (++j < n) {
+        var source = subgroups[j * n + i],
+            target = subgroups[i * n + j];
+        if (source.value || target.value) {
+          chords.push(source.value < target.value
+              ? {source: target, target: source}
+              : {source: source, target: target});
+        }
+      }
+    }
+
+    return sortChords ? chords.sort(sortChords) : chords;
+  }
+
+  chord.padAngle = function(_) {
+    return arguments.length ? (padAngle = max$1(0, _), chord) : padAngle;
+  };
+
+  chord.sortGroups = function(_) {
+    return arguments.length ? (sortGroups = _, chord) : sortGroups;
+  };
+
+  chord.sortSubgroups = function(_) {
+    return arguments.length ? (sortSubgroups = _, chord) : sortSubgroups;
+  };
+
+  chord.sortChords = function(_) {
+    return arguments.length ? (_ == null ? sortChords = null : (sortChords = compareValue(_))._ = _, chord) : sortChords && sortChords._;
+  };
+
+  return chord;
+}
+
+var slice$2 = Array.prototype.slice;
+
+function constant$5(x) {
+  return function() {
+    return x;
+  };
+}
+
+var pi$2 = Math.PI,
+    tau$2 = 2 * pi$2,
+    epsilon$1 = 1e-6,
+    tauEpsilon = tau$2 - epsilon$1;
+
+function Path() {
+  this._x0 = this._y0 = // start of current subpath
+  this._x1 = this._y1 = null; // end of current subpath
+  this._ = "";
+}
+
+function path() {
+  return new Path;
+}
+
+Path.prototype = path.prototype = {
+  constructor: Path,
+  moveTo: function(x, y) {
+    this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
+  },
+  closePath: function() {
+    if (this._x1 !== null) {
+      this._x1 = this._x0, this._y1 = this._y0;
+      this._ += "Z";
+    }
+  },
+  lineTo: function(x, y) {
+    this._ += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
+  },
+  quadraticCurveTo: function(x1, y1, x, y) {
+    this._ += "Q" + (+x1) + "," + (+y1) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
+  },
+  bezierCurveTo: function(x1, y1, x2, y2, x, y) {
+    this._ += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
+  },
+  arcTo: function(x1, y1, x2, y2, r) {
+    x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;
+    var x0 = this._x1,
+        y0 = this._y1,
+        x21 = x2 - x1,
+        y21 = y2 - y1,
+        x01 = x0 - x1,
+        y01 = y0 - y1,
+        l01_2 = x01 * x01 + y01 * y01;
+
+    // Is the radius negative? Error.
+    if (r < 0) throw new Error("negative radius: " + r);
+
+    // Is this path empty? Move to (x1,y1).
+    if (this._x1 === null) {
+      this._ += "M" + (this._x1 = x1) + "," + (this._y1 = y1);
+    }
+
+    // Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
+    else if (!(l01_2 > epsilon$1));
+
+    // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
+    // Equivalently, is (x1,y1) coincident with (x2,y2)?
+    // Or, is the radius zero? Line to (x1,y1).
+    else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon$1) || !r) {
+      this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1);
+    }
+
+    // Otherwise, draw an arc!
+    else {
+      var x20 = x2 - x0,
+          y20 = y2 - y0,
+          l21_2 = x21 * x21 + y21 * y21,
+          l20_2 = x20 * x20 + y20 * y20,
+          l21 = Math.sqrt(l21_2),
+          l01 = Math.sqrt(l01_2),
+          l = r * Math.tan((pi$2 - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2),
+          t01 = l / l01,
+          t21 = l / l21;
+
+      // If the start tangent is not coincident with (x0,y0), line to.
+      if (Math.abs(t01 - 1) > epsilon$1) {
+        this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01);
+      }
+
+      this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21);
+    }
+  },
+  arc: function(x, y, r, a0, a1, ccw) {
+    x = +x, y = +y, r = +r;
+    var dx = r * Math.cos(a0),
+        dy = r * Math.sin(a0),
+        x0 = x + dx,
+        y0 = y + dy,
+        cw = 1 ^ ccw,
+        da = ccw ? a0 - a1 : a1 - a0;
+
+    // Is the radius negative? Error.
+    if (r < 0) throw new Error("negative radius: " + r);
+
+    // Is this path empty? Move to (x0,y0).
+    if (this._x1 === null) {
+      this._ += "M" + x0 + "," + y0;
+    }
+
+    // Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0).
+    else if (Math.abs(this._x1 - x0) > epsilon$1 || Math.abs(this._y1 - y0) > epsilon$1) {
+      this._ += "L" + x0 + "," + y0;
+    }
+
+    // Is this arc empty? We’re done.
+    if (!r) return;
+
+    // Does the angle go the wrong way? Flip the direction.
+    if (da < 0) da = da % tau$2 + tau$2;
+
+    // Is this a complete circle? Draw two arcs to complete the circle.
+    if (da > tauEpsilon) {
+      this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0);
+    }
+
+    // Is this arc non-empty? Draw an arc!
+    else if (da > epsilon$1) {
+      this._ += "A" + r + "," + r + ",0," + (+(da >= pi$2)) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1));
+    }
+  },
+  rect: function(x, y, w, h) {
+    this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + (+w) + "v" + (+h) + "h" + (-w) + "Z";
+  },
+  toString: function() {
+    return this._;
+  }
+};
+
+function defaultSource(d) {
+  return d.source;
+}
+
+function defaultTarget(d) {
+  return d.target;
+}
+
+function defaultRadius(d) {
+  return d.radius;
+}
+
+function defaultStartAngle(d) {
+  return d.startAngle;
+}
+
+function defaultEndAngle(d) {
+  return d.endAngle;
+}
+
+function ribbon() {
+  var source = defaultSource,
+      target = defaultTarget,
+      radius = defaultRadius,
+      startAngle = defaultStartAngle,
+      endAngle = defaultEndAngle,
+      context = null;
+
+  function ribbon() {
+    var buffer,
+        argv = slice$2.call(arguments),
+        s = source.apply(this, argv),
+        t = target.apply(this, argv),
+        sr = +radius.apply(this, (argv[0] = s, argv)),
+        sa0 = startAngle.apply(this, argv) - halfPi$1,
+        sa1 = endAngle.apply(this, argv) - halfPi$1,
+        sx0 = sr * cos(sa0),
+        sy0 = sr * sin(sa0),
+        tr = +radius.apply(this, (argv[0] = t, argv)),
+        ta0 = startAngle.apply(this, argv) - halfPi$1,
+        ta1 = endAngle.apply(this, argv) - halfPi$1;
+
+    if (!context) context = buffer = path();
+
+    context.moveTo(sx0, sy0);
+    context.arc(0, 0, sr, sa0, sa1);
+    if (sa0 !== ta0 || sa1 !== ta1) { // TODO sr !== tr?
+      context.quadraticCurveTo(0, 0, tr * cos(ta0), tr * sin(ta0));
+      context.arc(0, 0, tr, ta0, ta1);
+    }
+    context.quadraticCurveTo(0, 0, sx0, sy0);
+    context.closePath();
+
+    if (buffer) return context = null, buffer + "" || null;
+  }
+
+  ribbon.radius = function(_) {
+    return arguments.length ? (radius = typeof _ === "function" ? _ : constant$5(+_), ribbon) : radius;
+  };
+
+  ribbon.startAngle = function(_) {
+    return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$5(+_), ribbon) : startAngle;
+  };
+
+  ribbon.endAngle = function(_) {
+    return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$5(+_), ribbon) : endAngle;
+  };
+
+  ribbon.source = function(_) {
+    return arguments.length ? (source = _, ribbon) : source;
+  };
+
+  ribbon.target = function(_) {
+    return arguments.length ? (target = _, ribbon) : target;
+  };
+
+  ribbon.context = function(_) {
+    return arguments.length ? ((context = _ == null ? null : _), ribbon) : context;
+  };
+
+  return ribbon;
+}
+
+var prefix = "$";
+
+function Map() {}
+
+Map.prototype = map$1.prototype = {
+  constructor: Map,
+  has: function(key) {
+    return (prefix + key) in this;
+  },
+  get: function(key) {
+    return this[prefix + key];
+  },
+  set: function(key, value) {
+    this[prefix + key] = value;
+    return this;
+  },
+  remove: function(key) {
+    var property = prefix + key;
+    return property in this && delete this[property];
+  },
+  clear: function() {
+    for (var property in this) if (property[0] === prefix) delete this[property];
+  },
+  keys: function() {
+    var keys = [];
+    for (var property in this) if (property[0] === prefix) keys.push(property.slice(1));
+    return keys;
+  },
+  values: function() {
+    var values = [];
+    for (var property in this) if (property[0] === prefix) values.push(this[property]);
+    return values;
+  },
+  entries: function() {
+    var entries = [];
+    for (var property in this) if (property[0] === prefix) entries.push({key: property.slice(1), value: this[property]});
+    return entries;
+  },
+  size: function() {
+    var size = 0;
+    for (var property in this) if (property[0] === prefix) ++size;
+    return size;
+  },
+  empty: function() {
+    for (var property in this) if (property[0] === prefix) return false;
+    return true;
+  },
+  each: function(f) {
+    for (var property in this) if (property[0] === prefix) f(this[property], property.slice(1), this);
+  }
+};
+
+function map$1(object, f) {
+  var map = new Map;
+
+  // Copy constructor.
+  if (object instanceof Map) object.each(function(value, key) { map.set(key, value); });
+
+  // Index array by numeric index or specified key function.
+  else if (Array.isArray(object)) {
+    var i = -1,
+        n = object.length,
+        o;
+
+    if (f == null) while (++i < n) map.set(i, object[i]);
+    else while (++i < n) map.set(f(o = object[i], i, object), o);
+  }
+
+  // Convert object to map.
+  else if (object) for (var key in object) map.set(key, object[key]);
+
+  return map;
+}
+
+function nest() {
+  var keys = [],
+      sortKeys = [],
+      sortValues,
+      rollup,
+      nest;
+
+  function apply(array, depth, createResult, setResult) {
+    if (depth >= keys.length) {
+      if (sortValues != null) array.sort(sortValues);
+      return rollup != null ? rollup(array) : array;
+    }
+
+    var i = -1,
+        n = array.length,
+        key = keys[depth++],
+        keyValue,
+        value,
+        valuesByKey = map$1(),
+        values,
+        result = createResult();
+
+    while (++i < n) {
+      if (values = valuesByKey.get(keyValue = key(value = array[i]) + "")) {
+        values.push(value);
+      } else {
+        valuesByKey.set(keyValue, [value]);
+      }
+    }
+
+    valuesByKey.each(function(values, key) {
+      setResult(result, key, apply(values, depth, createResult, setResult));
+    });
+
+    return result;
+  }
+
+  function entries(map, depth) {
+    if (++depth > keys.length) return map;
+    var array, sortKey = sortKeys[depth - 1];
+    if (rollup != null && depth >= keys.length) array = map.entries();
+    else array = [], map.each(function(v, k) { array.push({key: k, values: entries(v, depth)}); });
+    return sortKey != null ? array.sort(function(a, b) { return sortKey(a.key, b.key); }) : array;
+  }
+
+  return nest = {
+    object: function(array) { return apply(array, 0, createObject, setObject); },
+    map: function(array) { return apply(array, 0, createMap, setMap); },
+    entries: function(array) { return entries(apply(array, 0, createMap, setMap), 0); },
+    key: function(d) { keys.push(d); return nest; },
+    sortKeys: function(order) { sortKeys[keys.length - 1] = order; return nest; },
+    sortValues: function(order) { sortValues = order; return nest; },
+    rollup: function(f) { rollup = f; return nest; }
+  };
+}
+
+function createObject() {
+  return {};
+}
+
+function setObject(object, key, value) {
+  object[key] = value;
+}
+
+function createMap() {
+  return map$1();
+}
+
+function setMap(map, key, value) {
+  map.set(key, value);
+}
+
+function Set() {}
+
+var proto = map$1.prototype;
+
+Set.prototype = set$2.prototype = {
+  constructor: Set,
+  has: proto.has,
+  add: function(value) {
+    value += "";
+    this[prefix + value] = value;
+    return this;
+  },
+  remove: proto.remove,
+  clear: proto.clear,
+  values: proto.keys,
+  size: proto.size,
+  empty: proto.empty,
+  each: proto.each
+};
+
+function set$2(object, f) {
+  var set = new Set;
+
+  // Copy constructor.
+  if (object instanceof Set) object.each(function(value) { set.add(value); });
+
+  // Otherwise, assume it’s an array.
+  else if (object) {
+    var i = -1, n = object.length;
+    if (f == null) while (++i < n) set.add(object[i]);
+    else while (++i < n) set.add(f(object[i], i, object));
+  }
+
+  return set;
+}
+
+function keys(map) {
+  var keys = [];
+  for (var key in map) keys.push(key);
+  return keys;
+}
+
+function values(map) {
+  var values = [];
+  for (var key in map) values.push(map[key]);
+  return values;
+}
+
+function entries(map) {
+  var entries = [];
+  for (var key in map) entries.push({key: key, value: map[key]});
+  return entries;
+}
+
+var array$2 = Array.prototype;
+
+var slice$3 = array$2.slice;
+
+function ascending$2(a, b) {
+  return a - b;
+}
+
+function area(ring) {
+  var i = 0, n = ring.length, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1];
+  while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1];
+  return area;
+}
+
+function constant$6(x) {
+  return function() {
+    return x;
+  };
+}
+
+function contains(ring, hole) {
+  var i = -1, n = hole.length, c;
+  while (++i < n) if (c = ringContains(ring, hole[i])) return c;
+  return 0;
+}
+
+function ringContains(ring, point) {
+  var x = point[0], y = point[1], contains = -1;
+  for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) {
+    var pi = ring[i], xi = pi[0], yi = pi[1], pj = ring[j], xj = pj[0], yj = pj[1];
+    if (segmentContains(pi, pj, point)) return 0;
+    if (((yi > y) !== (yj > y)) && ((x < (xj - xi) * (y - yi) / (yj - yi) + xi))) contains = -contains;
+  }
+  return contains;
+}
+
+function segmentContains(a, b, c) {
+  var i; return collinear(a, b, c) && within(a[i = +(a[0] === b[0])], c[i], b[i]);
+}
+
+function collinear(a, b, c) {
+  return (b[0] - a[0]) * (c[1] - a[1]) === (c[0] - a[0]) * (b[1] - a[1]);
+}
+
+function within(p, q, r) {
+  return p <= q && q <= r || r <= q && q <= p;
+}
+
+function noop$1() {}
+
+var cases = [
+  [],
+  [[[1.0, 1.5], [0.5, 1.0]]],
+  [[[1.5, 1.0], [1.0, 1.5]]],
+  [[[1.5, 1.0], [0.5, 1.0]]],
+  [[[1.0, 0.5], [1.5, 1.0]]],
+  [[[1.0, 1.5], [0.5, 1.0]], [[1.0, 0.5], [1.5, 1.0]]],
+  [[[1.0, 0.5], [1.0, 1.5]]],
+  [[[1.0, 0.5], [0.5, 1.0]]],
+  [[[0.5, 1.0], [1.0, 0.5]]],
+  [[[1.0, 1.5], [1.0, 0.5]]],
+  [[[0.5, 1.0], [1.0, 0.5]], [[1.5, 1.0], [1.0, 1.5]]],
+  [[[1.5, 1.0], [1.0, 0.5]]],
+  [[[0.5, 1.0], [1.5, 1.0]]],
+  [[[1.0, 1.5], [1.5, 1.0]]],
+  [[[0.5, 1.0], [1.0, 1.5]]],
+  []
+];
+
+function contours() {
+  var dx = 1,
+      dy = 1,
+      threshold$$1 = thresholdSturges,
+      smooth = smoothLinear;
+
+  function contours(values) {
+    var tz = threshold$$1(values);
+
+    // Convert number of thresholds into uniform thresholds.
+    if (!Array.isArray(tz)) {
+      var domain = extent(values), start = domain[0], stop = domain[1];
+      tz = tickStep(start, stop, tz);
+      tz = sequence(Math.floor(start / tz) * tz, Math.floor(stop / tz) * tz, tz);
+    } else {
+      tz = tz.slice().sort(ascending$2);
+    }
+
+    return tz.map(function(value) {
+      return contour(values, value);
+    });
+  }
+
+  // Accumulate, smooth contour rings, assign holes to exterior rings.
+  // Based on https://github.com/mbostock/shapefile/blob/v0.6.2/shp/polygon.js
+  function contour(values, value) {
+    var polygons = [],
+        holes = [];
+
+    isorings(values, value, function(ring) {
+      smooth(ring, values, value);
+      if (area(ring) > 0) polygons.push([ring]);
+      else holes.push(ring);
+    });
+
+    holes.forEach(function(hole) {
+      for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
+        if (contains((polygon = polygons[i])[0], hole) !== -1) {
+          polygon.push(hole);
+          return;
+        }
+      }
+    });
+
+    return {
+      type: "MultiPolygon",
+      value: value,
+      coordinates: polygons
+    };
+  }
+
+  // Marching squares with isolines stitched into rings.
+  // Based on https://github.com/topojson/topojson-client/blob/v3.0.0/src/stitch.js
+  function isorings(values, value, callback) {
+    var fragmentByStart = new Array,
+        fragmentByEnd = new Array,
+        x, y, t0, t1, t2, t3;
+
+    // Special case for the first row (y = -1, t2 = t3 = 0).
+    x = y = -1;
+    t1 = values[0] >= value;
+    cases[t1 << 1].forEach(stitch);
+    while (++x < dx - 1) {
+      t0 = t1, t1 = values[x + 1] >= value;
+      cases[t0 | t1 << 1].forEach(stitch);
+    }
+    cases[t1 << 0].forEach(stitch);
+
+    // General case for the intermediate rows.
+    while (++y < dy - 1) {
+      x = -1;
+      t1 = values[y * dx + dx] >= value;
+      t2 = values[y * dx] >= value;
+      cases[t1 << 1 | t2 << 2].forEach(stitch);
+      while (++x < dx - 1) {
+        t0 = t1, t1 = values[y * dx + dx + x + 1] >= value;
+        t3 = t2, t2 = values[y * dx + x + 1] >= value;
+        cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
+      }
+      cases[t1 | t2 << 3].forEach(stitch);
+    }
+
+    // Special case for the last row (y = dy - 1, t0 = t1 = 0).
+    x = -1;
+    t2 = values[y * dx] >= value;
+    cases[t2 << 2].forEach(stitch);
+    while (++x < dx - 1) {
+      t3 = t2, t2 = values[y * dx + x + 1] >= value;
+      cases[t2 << 2 | t3 << 3].forEach(stitch);
+    }
+    cases[t2 << 3].forEach(stitch);
+
+    function stitch(line) {
+      var start = [line[0][0] + x, line[0][1] + y],
+          end = [line[1][0] + x, line[1][1] + y],
+          startIndex = index(start),
+          endIndex = index(end),
+          f, g;
+      if (f = fragmentByEnd[startIndex]) {
+        if (g = fragmentByStart[endIndex]) {
+          delete fragmentByEnd[f.end];
+          delete fragmentByStart[g.start];
+          if (f === g) {
+            f.ring.push(end);
+            callback(f.ring);
+          } else {
+            fragmentByStart[f.start] = fragmentByEnd[g.end] = {start: f.start, end: g.end, ring: f.ring.concat(g.ring)};
+          }
+        } else {
+          delete fragmentByEnd[f.end];
+          f.ring.push(end);
+          fragmentByEnd[f.end = endIndex] = f;
+        }
+      } else if (f = fragmentByStart[endIndex]) {
+        if (g = fragmentByEnd[startIndex]) {
+          delete fragmentByStart[f.start];
+          delete fragmentByEnd[g.end];
+          if (f === g) {
+            f.ring.push(end);
+            callback(f.ring);
+          } else {
+            fragmentByStart[g.start] = fragmentByEnd[f.end] = {start: g.start, end: f.end, ring: g.ring.concat(f.ring)};
+          }
+        } else {
+          delete fragmentByStart[f.start];
+          f.ring.unshift(start);
+          fragmentByStart[f.start = startIndex] = f;
+        }
+      } else {
+        fragmentByStart[startIndex] = fragmentByEnd[endIndex] = {start: startIndex, end: endIndex, ring: [start, end]};
+      }
+    }
+  }
+
+  function index(point) {
+    return point[0] * 2 + point[1] * (dx + 1) * 4;
+  }
+
+  function smoothLinear(ring, values, value) {
+    ring.forEach(function(point) {
+      var x = point[0],
+          y = point[1],
+          xt = x | 0,
+          yt = y | 0,
+          v0,
+          v1 = values[yt * dx + xt];
+      if (x > 0 && x < dx && xt === x) {
+        v0 = values[yt * dx + xt - 1];
+        point[0] = x + (value - v0) / (v1 - v0) - 0.5;
+      }
+      if (y > 0 && y < dy && yt === y) {
+        v0 = values[(yt - 1) * dx + xt];
+        point[1] = y + (value - v0) / (v1 - v0) - 0.5;
+      }
+    });
+  }
+
+  contours.contour = contour;
+
+  contours.size = function(_) {
+    if (!arguments.length) return [dx, dy];
+    var _0 = Math.ceil(_[0]), _1 = Math.ceil(_[1]);
+    if (!(_0 > 0) || !(_1 > 0)) throw new Error("invalid size");
+    return dx = _0, dy = _1, contours;
+  };
+
+  contours.thresholds = function(_) {
+    return arguments.length ? (threshold$$1 = typeof _ === "function" ? _ : Array.isArray(_) ? constant$6(slice$3.call(_)) : constant$6(_), contours) : threshold$$1;
+  };
+
+  contours.smooth = function(_) {
+    return arguments.length ? (smooth = _ ? smoothLinear : noop$1, contours) : smooth === smoothLinear;
+  };
+
+  return contours;
+}
+
+// TODO Optimize edge cases.
+// TODO Optimize index calculation.
+// TODO Optimize arguments.
+function blurX(source, target, r) {
+  var n = source.width,
+      m = source.height,
+      w = (r << 1) + 1;
+  for (var j = 0; j < m; ++j) {
+    for (var i = 0, sr = 0; i < n + r; ++i) {
+      if (i < n) {
+        sr += source.data[i + j * n];
+      }
+      if (i >= r) {
+        if (i >= w) {
+          sr -= source.data[i - w + j * n];
+        }
+        target.data[i - r + j * n] = sr / Math.min(i + 1, n - 1 + w - i, w);
+      }
+    }
+  }
+}
+
+// TODO Optimize edge cases.
+// TODO Optimize index calculation.
+// TODO Optimize arguments.
+function blurY(source, target, r) {
+  var n = source.width,
+      m = source.height,
+      w = (r << 1) + 1;
+  for (var i = 0; i < n; ++i) {
+    for (var j = 0, sr = 0; j < m + r; ++j) {
+      if (j < m) {
+        sr += source.data[i + j * n];
+      }
+      if (j >= r) {
+        if (j >= w) {
+          sr -= source.data[i + (j - w) * n];
+        }
+        target.data[i + (j - r) * n] = sr / Math.min(j + 1, m - 1 + w - j, w);
+      }
+    }
+  }
+}
+
+function defaultX(d) {
+  return d[0];
+}
+
+function defaultY(d) {
+  return d[1];
+}
+
+function defaultWeight() {
+  return 1;
+}
+
+function density() {
+  var x = defaultX,
+      y = defaultY,
+      weight = defaultWeight,
+      dx = 960,
+      dy = 500,
+      r = 20, // blur radius
+      k = 2, // log2(grid cell size)
+      o = r * 3, // grid offset, to pad for blur
+      n = (dx + o * 2) >> k, // grid width
+      m = (dy + o * 2) >> k, // grid height
+      threshold$$1 = constant$6(20);
+
+  function density(data) {
+    var values0 = new Float32Array(n * m),
+        values1 = new Float32Array(n * m);
+
+    data.forEach(function(d, i, data) {
+      var xi = (+x(d, i, data) + o) >> k,
+          yi = (+y(d, i, data) + o) >> k,
+          wi = +weight(d, i, data);
+      if (xi >= 0 && xi < n && yi >= 0 && yi < m) {
+        values0[xi + yi * n] += wi;
+      }
+    });
+
+    // TODO Optimize.
+    blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+    blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+    blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+    blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+    blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+    blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+
+    var tz = threshold$$1(values0);
+
+    // Convert number of thresholds into uniform thresholds.
+    if (!Array.isArray(tz)) {
+      var stop = max(values0);
+      tz = tickStep(0, stop, tz);
+      tz = sequence(0, Math.floor(stop / tz) * tz, tz);
+      tz.shift();
+    }
+
+    return contours()
+        .thresholds(tz)
+        .size([n, m])
+      (values0)
+        .map(transform);
+  }
+
+  function transform(geometry) {
+    geometry.value *= Math.pow(2, -2 * k); // Density in points per square pixel.
+    geometry.coordinates.forEach(transformPolygon);
+    return geometry;
+  }
+
+  function transformPolygon(coordinates) {
+    coordinates.forEach(transformRing);
+  }
+
+  function transformRing(coordinates) {
+    coordinates.forEach(transformPoint);
+  }
+
+  // TODO Optimize.
+  function transformPoint(coordinates) {
+    coordinates[0] = coordinates[0] * Math.pow(2, k) - o;
+    coordinates[1] = coordinates[1] * Math.pow(2, k) - o;
+  }
+
+  function resize() {
+    o = r * 3;
+    n = (dx + o * 2) >> k;
+    m = (dy + o * 2) >> k;
+    return density;
+  }
+
+  density.x = function(_) {
+    return arguments.length ? (x = typeof _ === "function" ? _ : constant$6(+_), density) : x;
+  };
+
+  density.y = function(_) {
+    return arguments.length ? (y = typeof _ === "function" ? _ : constant$6(+_), density) : y;
+  };
+
+  density.weight = function(_) {
+    return arguments.length ? (weight = typeof _ === "function" ? _ : constant$6(+_), density) : weight;
+  };
+
+  density.size = function(_) {
+    if (!arguments.length) return [dx, dy];
+    var _0 = Math.ceil(_[0]), _1 = Math.ceil(_[1]);
+    if (!(_0 >= 0) && !(_0 >= 0)) throw new Error("invalid size");
+    return dx = _0, dy = _1, resize();
+  };
+
+  density.cellSize = function(_) {
+    if (!arguments.length) return 1 << k;
+    if (!((_ = +_) >= 1)) throw new Error("invalid cell size");
+    return k = Math.floor(Math.log(_) / Math.LN2), resize();
+  };
+
+  density.thresholds = function(_) {
+    return arguments.length ? (threshold$$1 = typeof _ === "function" ? _ : Array.isArray(_) ? constant$6(slice$3.call(_)) : constant$6(_), density) : threshold$$1;
+  };
+
+  density.bandwidth = function(_) {
+    if (!arguments.length) return Math.sqrt(r * (r + 1));
+    if (!((_ = +_) >= 0)) throw new Error("invalid bandwidth");
+    return r = Math.round((Math.sqrt(4 * _ * _ + 1) - 1) / 2), resize();
+  };
+
+  return density;
+}
+
+var EOL = {},
+    EOF = {},
+    QUOTE = 34,
+    NEWLINE = 10,
+    RETURN = 13;
+
+function objectConverter(columns) {
+  return new Function("d", "return {" + columns.map(function(name, i) {
+    return JSON.stringify(name) + ": d[" + i + "]";
+  }).join(",") + "}");
+}
+
+function customConverter(columns, f) {
+  var object = objectConverter(columns);
+  return function(row, i) {
+    return f(object(row), i, columns);
+  };
+}
+
+// Compute unique columns in order of discovery.
+function inferColumns(rows) {
+  var columnSet = Object.create(null),
+      columns = [];
+
+  rows.forEach(function(row) {
+    for (var column in row) {
+      if (!(column in columnSet)) {
+        columns.push(columnSet[column] = column);
+      }
+    }
+  });
+
+  return columns;
+}
+
+function pad(value, width) {
+  var s = value + "", length = s.length;
+  return length < width ? new Array(width - length + 1).join(0) + s : s;
+}
+
+function formatYear(year) {
+  return year < 0 ? "-" + pad(-year, 6)
+    : year > 9999 ? "+" + pad(year, 6)
+    : pad(year, 4);
+}
+
+function formatDate(date) {
+  var hours = date.getUTCHours(),
+      minutes = date.getUTCMinutes(),
+      seconds = date.getUTCSeconds(),
+      milliseconds = date.getUTCMilliseconds();
+  return isNaN(date) ? "Invalid Date"
+      : formatYear(date.getUTCFullYear(), 4) + "-" + pad(date.getUTCMonth() + 1, 2) + "-" + pad(date.getUTCDate(), 2)
+      + (milliseconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3) + "Z"
+      : seconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "Z"
+      : minutes || hours ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + "Z"
+      : "");
+}
+
+function dsvFormat(delimiter) {
+  var reFormat = new RegExp("[\"" + delimiter + "\n\r]"),
+      DELIMITER = delimiter.charCodeAt(0);
+
+  function parse(text, f) {
+    var convert, columns, rows = parseRows(text, function(row, i) {
+      if (convert) return convert(row, i - 1);
+      columns = row, convert = f ? customConverter(row, f) : objectConverter(row);
+    });
+    rows.columns = columns || [];
+    return rows;
+  }
+
+  function parseRows(text, f) {
+    var rows = [], // output rows
+        N = text.length,
+        I = 0, // current character index
+        n = 0, // current line number
+        t, // current token
+        eof = N <= 0, // current token followed by EOF?
+        eol = false; // current token followed by EOL?
+
+    // Strip the trailing newline.
+    if (text.charCodeAt(N - 1) === NEWLINE) --N;
+    if (text.charCodeAt(N - 1) === RETURN) --N;
+
+    function token() {
+      if (eof) return EOF;
+      if (eol) return eol = false, EOL;
+
+      // Unescape quotes.
+      var i, j = I, c;
+      if (text.charCodeAt(j) === QUOTE) {
+        while (I++ < N && text.charCodeAt(I) !== QUOTE || text.charCodeAt(++I) === QUOTE);
+        if ((i = I) >= N) eof = true;
+        else if ((c = text.charCodeAt(I++)) === NEWLINE) eol = true;
+        else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }
+        return text.slice(j + 1, i - 1).replace(/""/g, "\"");
+      }
+
+      // Find next delimiter or newline.
+      while (I < N) {
+        if ((c = text.charCodeAt(i = I++)) === NEWLINE) eol = true;
+        else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }
+        else if (c !== DELIMITER) continue;
+        return text.slice(j, i);
+      }
+
+      // Return last token before EOF.
+      return eof = true, text.slice(j, N);
+    }
+
+    while ((t = token()) !== EOF) {
+      var row = [];
+      while (t !== EOL && t !== EOF) row.push(t), t = token();
+      if (f && (row = f(row, n++)) == null) continue;
+      rows.push(row);
+    }
+
+    return rows;
+  }
+
+  function preformatBody(rows, columns) {
+    return rows.map(function(row) {
+      return columns.map(function(column) {
+        return formatValue(row[column]);
+      }).join(delimiter);
+    });
+  }
+
+  function format(rows, columns) {
+    if (columns == null) columns = inferColumns(rows);
+    return [columns.map(formatValue).join(delimiter)].concat(preformatBody(rows, columns)).join("\n");
+  }
+
+  function formatBody(rows, columns) {
+    if (columns == null) columns = inferColumns(rows);
+    return preformatBody(rows, columns).join("\n");
+  }
+
+  function formatRows(rows) {
+    return rows.map(formatRow).join("\n");
+  }
+
+  function formatRow(row) {
+    return row.map(formatValue).join(delimiter);
+  }
+
+  function formatValue(value) {
+    return value == null ? ""
+        : value instanceof Date ? formatDate(value)
+        : reFormat.test(value += "") ? "\"" + value.replace(/"/g, "\"\"") + "\""
+        : value;
+  }
+
+  return {
+    parse: parse,
+    parseRows: parseRows,
+    format: format,
+    formatBody: formatBody,
+    formatRows: formatRows
+  };
+}
+
+var csv = dsvFormat(",");
+
+var csvParse = csv.parse;
+var csvParseRows = csv.parseRows;
+var csvFormat = csv.format;
+var csvFormatBody = csv.formatBody;
+var csvFormatRows = csv.formatRows;
+
+var tsv = dsvFormat("\t");
+
+var tsvParse = tsv.parse;
+var tsvParseRows = tsv.parseRows;
+var tsvFormat = tsv.format;
+var tsvFormatBody = tsv.formatBody;
+var tsvFormatRows = tsv.formatRows;
+
+function autoType(object) {
+  for (var key in object) {
+    var value = object[key].trim(), number;
+    if (!value) value = null;
+    else if (value === "true") value = true;
+    else if (value === "false") value = false;
+    else if (value === "NaN") value = NaN;
+    else if (!isNaN(number = +value)) value = number;
+    else if (/^([-+]\d{2})?\d{4}(-\d{2}(-\d{2})?)?(T\d{2}:\d{2}(:\d{2}(\.\d{3})?)?(Z|[-+]\d{2}:\d{2})?)?$/.test(value)) value = new Date(value);
+    else continue;
+    object[key] = value;
+  }
+  return object;
+}
+
+function responseBlob(response) {
+  if (!response.ok) throw new Error(response.status + " " + response.statusText);
+  return response.blob();
+}
+
+function blob(input, init) {
+  return fetch(input, init).then(responseBlob);
+}
+
+function responseArrayBuffer(response) {
+  if (!response.ok) throw new Error(response.status + " " + response.statusText);
+  return response.arrayBuffer();
+}
+
+function buffer(input, init) {
+  return fetch(input, init).then(responseArrayBuffer);
+}
+
+function responseText(response) {
+  if (!response.ok) throw new Error(response.status + " " + response.statusText);
+  return response.text();
+}
+
+function text(input, init) {
+  return fetch(input, init).then(responseText);
+}
+
+function dsvParse(parse) {
+  return function(input, init, row) {
+    if (arguments.length === 2 && typeof init === "function") row = init, init = undefined;
+    return text(input, init).then(function(response) {
+      return parse(response, row);
+    });
+  };
+}
+
+function dsv(delimiter, input, init, row) {
+  if (arguments.length === 3 && typeof init === "function") row = init, init = undefined;
+  var format = dsvFormat(delimiter);
+  return text(input, init).then(function(response) {
+    return format.parse(response, row);
+  });
+}
+
+var csv$1 = dsvParse(csvParse);
+var tsv$1 = dsvParse(tsvParse);
+
+function image(input, init) {
+  return new Promise(function(resolve, reject) {
+    var image = new Image;
+    for (var key in init) image[key] = init[key];
+    image.onerror = reject;
+    image.onload = function() { resolve(image); };
+    image.src = input;
+  });
+}
+
+function responseJson(response) {
+  if (!response.ok) throw new Error(response.status + " " + response.statusText);
+  return response.json();
+}
+
+function json(input, init) {
+  return fetch(input, init).then(responseJson);
+}
+
+function parser(type) {
+  return function(input, init)  {
+    return text(input, init).then(function(text$$1) {
+      return (new DOMParser).parseFromString(text$$1, type);
+    });
+  };
+}
+
+var xml = parser("application/xml");
+
+var html = parser("text/html");
+
+var svg = parser("image/svg+xml");
+
+function center$1(x, y) {
+  var nodes;
+
+  if (x == null) x = 0;
+  if (y == null) y = 0;
+
+  function force() {
+    var i,
+        n = nodes.length,
+        node,
+        sx = 0,
+        sy = 0;
+
+    for (i = 0; i < n; ++i) {
+      node = nodes[i], sx += node.x, sy += node.y;
+    }
+
+    for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) {
+      node = nodes[i], node.x -= sx, node.y -= sy;
+    }
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+  };
+
+  force.x = function(_) {
+    return arguments.length ? (x = +_, force) : x;
+  };
+
+  force.y = function(_) {
+    return arguments.length ? (y = +_, force) : y;
+  };
+
+  return force;
+}
+
+function constant$7(x) {
+  return function() {
+    return x;
+  };
+}
+
+function jiggle() {
+  return (Math.random() - 0.5) * 1e-6;
+}
+
+function tree_add(d) {
+  var x = +this._x.call(null, d),
+      y = +this._y.call(null, d);
+  return add(this.cover(x, y), x, y, d);
+}
+
+function add(tree, x, y, d) {
+  if (isNaN(x) || isNaN(y)) return tree; // ignore invalid points
+
+  var parent,
+      node = tree._root,
+      leaf = {data: d},
+      x0 = tree._x0,
+      y0 = tree._y0,
+      x1 = tree._x1,
+      y1 = tree._y1,
+      xm,
+      ym,
+      xp,
+      yp,
+      right,
+      bottom,
+      i,
+      j;
+
+  // If the tree is empty, initialize the root as a leaf.
+  if (!node) return tree._root = leaf, tree;
+
+  // Find the existing leaf for the new point, or add it.
+  while (node.length) {
+    if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+    if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+    if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;
+  }
+
+  // Is the new point is exactly coincident with the existing point?
+  xp = +tree._x.call(null, node.data);
+  yp = +tree._y.call(null, node.data);
+  if (x === xp && y === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;
+
+  // Otherwise, split the leaf node until the old and new point are separated.
+  do {
+    parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);
+    if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+    if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+  } while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm)));
+  return parent[j] = node, parent[i] = leaf, tree;
+}
+
+function addAll(data) {
+  var d, i, n = data.length,
+      x,
+      y,
+      xz = new Array(n),
+      yz = new Array(n),
+      x0 = Infinity,
+      y0 = Infinity,
+      x1 = -Infinity,
+      y1 = -Infinity;
+
+  // Compute the points and their extent.
+  for (i = 0; i < n; ++i) {
+    if (isNaN(x = +this._x.call(null, d = data[i])) || isNaN(y = +this._y.call(null, d))) continue;
+    xz[i] = x;
+    yz[i] = y;
+    if (x < x0) x0 = x;
+    if (x > x1) x1 = x;
+    if (y < y0) y0 = y;
+    if (y > y1) y1 = y;
+  }
+
+  // If there were no (valid) points, abort.
+  if (x0 > x1 || y0 > y1) return this;
+
+  // Expand the tree to cover the new points.
+  this.cover(x0, y0).cover(x1, y1);
+
+  // Add the new points.
+  for (i = 0; i < n; ++i) {
+    add(this, xz[i], yz[i], data[i]);
+  }
+
+  return this;
+}
+
+function tree_cover(x, y) {
+  if (isNaN(x = +x) || isNaN(y = +y)) return this; // ignore invalid points
+
+  var x0 = this._x0,
+      y0 = this._y0,
+      x1 = this._x1,
+      y1 = this._y1;
+
+  // If the quadtree has no extent, initialize them.
+  // Integer extent are necessary so that if we later double the extent,
+  // the existing quadrant boundaries don’t change due to floating point error!
+  if (isNaN(x0)) {
+    x1 = (x0 = Math.floor(x)) + 1;
+    y1 = (y0 = Math.floor(y)) + 1;
+  }
+
+  // Otherwise, double repeatedly to cover.
+  else {
+    var z = x1 - x0,
+        node = this._root,
+        parent,
+        i;
+
+    while (x0 > x || x >= x1 || y0 > y || y >= y1) {
+      i = (y < y0) << 1 | (x < x0);
+      parent = new Array(4), parent[i] = node, node = parent, z *= 2;
+      switch (i) {
+        case 0: x1 = x0 + z, y1 = y0 + z; break;
+        case 1: x0 = x1 - z, y1 = y0 + z; break;
+        case 2: x1 = x0 + z, y0 = y1 - z; break;
+        case 3: x0 = x1 - z, y0 = y1 - z; break;
+      }
+    }
+
+    if (this._root && this._root.length) this._root = node;
+  }
+
+  this._x0 = x0;
+  this._y0 = y0;
+  this._x1 = x1;
+  this._y1 = y1;
+  return this;
+}
+
+function tree_data() {
+  var data = [];
+  this.visit(function(node) {
+    if (!node.length) do data.push(node.data); while (node = node.next)
+  });
+  return data;
+}
+
+function tree_extent(_) {
+  return arguments.length
+      ? this.cover(+_[0][0], +_[0][1]).cover(+_[1][0], +_[1][1])
+      : isNaN(this._x0) ? undefined : [[this._x0, this._y0], [this._x1, this._y1]];
+}
+
+function Quad(node, x0, y0, x1, y1) {
+  this.node = node;
+  this.x0 = x0;
+  this.y0 = y0;
+  this.x1 = x1;
+  this.y1 = y1;
+}
+
+function tree_find(x, y, radius) {
+  var data,
+      x0 = this._x0,
+      y0 = this._y0,
+      x1,
+      y1,
+      x2,
+      y2,
+      x3 = this._x1,
+      y3 = this._y1,
+      quads = [],
+      node = this._root,
+      q,
+      i;
+
+  if (node) quads.push(new Quad(node, x0, y0, x3, y3));
+  if (radius == null) radius = Infinity;
+  else {
+    x0 = x - radius, y0 = y - radius;
+    x3 = x + radius, y3 = y + radius;
+    radius *= radius;
+  }
+
+  while (q = quads.pop()) {
+
+    // Stop searching if this quadrant can’t contain a closer node.
+    if (!(node = q.node)
+        || (x1 = q.x0) > x3
+        || (y1 = q.y0) > y3
+        || (x2 = q.x1) < x0
+        || (y2 = q.y1) < y0) continue;
+
+    // Bisect the current quadrant.
+    if (node.length) {
+      var xm = (x1 + x2) / 2,
+          ym = (y1 + y2) / 2;
+
+      quads.push(
+        new Quad(node[3], xm, ym, x2, y2),
+        new Quad(node[2], x1, ym, xm, y2),
+        new Quad(node[1], xm, y1, x2, ym),
+        new Quad(node[0], x1, y1, xm, ym)
+      );
+
+      // Visit the closest quadrant first.
+      if (i = (y >= ym) << 1 | (x >= xm)) {
+        q = quads[quads.length - 1];
+        quads[quads.length - 1] = quads[quads.length - 1 - i];
+        quads[quads.length - 1 - i] = q;
+      }
+    }
+
+    // Visit this point. (Visiting coincident points isn’t necessary!)
+    else {
+      var dx = x - +this._x.call(null, node.data),
+          dy = y - +this._y.call(null, node.data),
+          d2 = dx * dx + dy * dy;
+      if (d2 < radius) {
+        var d = Math.sqrt(radius = d2);
+        x0 = x - d, y0 = y - d;
+        x3 = x + d, y3 = y + d;
+        data = node.data;
+      }
+    }
+  }
+
+  return data;
+}
+
+function tree_remove(d) {
+  if (isNaN(x = +this._x.call(null, d)) || isNaN(y = +this._y.call(null, d))) return this; // ignore invalid points
+
+  var parent,
+      node = this._root,
+      retainer,
+      previous,
+      next,
+      x0 = this._x0,
+      y0 = this._y0,
+      x1 = this._x1,
+      y1 = this._y1,
+      x,
+      y,
+      xm,
+      ym,
+      right,
+      bottom,
+      i,
+      j;
+
+  // If the tree is empty, initialize the root as a leaf.
+  if (!node) return this;
+
+  // Find the leaf node for the point.
+  // While descending, also retain the deepest parent with a non-removed sibling.
+  if (node.length) while (true) {
+    if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+    if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+    if (!(parent = node, node = node[i = bottom << 1 | right])) return this;
+    if (!node.length) break;
+    if (parent[(i + 1) & 3] || parent[(i + 2) & 3] || parent[(i + 3) & 3]) retainer = parent, j = i;
+  }
+
+  // Find the point to remove.
+  while (node.data !== d) if (!(previous = node, node = node.next)) return this;
+  if (next = node.next) delete node.next;
+
+  // If there are multiple coincident points, remove just the point.
+  if (previous) return (next ? previous.next = next : delete previous.next), this;
+
+  // If this is the root point, remove it.
+  if (!parent) return this._root = next, this;
+
+  // Remove this leaf.
+  next ? parent[i] = next : delete parent[i];
+
+  // If the parent now contains exactly one leaf, collapse superfluous parents.
+  if ((node = parent[0] || parent[1] || parent[2] || parent[3])
+      && node === (parent[3] || parent[2] || parent[1] || parent[0])
+      && !node.length) {
+    if (retainer) retainer[j] = node;
+    else this._root = node;
+  }
+
+  return this;
+}
+
+function removeAll(data) {
+  for (var i = 0, n = data.length; i < n; ++i) this.remove(data[i]);
+  return this;
+}
+
+function tree_root() {
+  return this._root;
+}
+
+function tree_size() {
+  var size = 0;
+  this.visit(function(node) {
+    if (!node.length) do ++size; while (node = node.next)
+  });
+  return size;
+}
+
+function tree_visit(callback) {
+  var quads = [], q, node = this._root, child, x0, y0, x1, y1;
+  if (node) quads.push(new Quad(node, this._x0, this._y0, this._x1, this._y1));
+  while (q = quads.pop()) {
+    if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1) && node.length) {
+      var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
+      if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
+      if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
+      if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
+      if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
+    }
+  }
+  return this;
+}
+
+function tree_visitAfter(callback) {
+  var quads = [], next = [], q;
+  if (this._root) quads.push(new Quad(this._root, this._x0, this._y0, this._x1, this._y1));
+  while (q = quads.pop()) {
+    var node = q.node;
+    if (node.length) {
+      var child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
+      if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
+      if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
+      if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
+      if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
+    }
+    next.push(q);
+  }
+  while (q = next.pop()) {
+    callback(q.node, q.x0, q.y0, q.x1, q.y1);
+  }
+  return this;
+}
+
+function defaultX$1(d) {
+  return d[0];
+}
+
+function tree_x(_) {
+  return arguments.length ? (this._x = _, this) : this._x;
+}
+
+function defaultY$1(d) {
+  return d[1];
+}
+
+function tree_y(_) {
+  return arguments.length ? (this._y = _, this) : this._y;
+}
+
+function quadtree(nodes, x, y) {
+  var tree = new Quadtree(x == null ? defaultX$1 : x, y == null ? defaultY$1 : y, NaN, NaN, NaN, NaN);
+  return nodes == null ? tree : tree.addAll(nodes);
+}
+
+function Quadtree(x, y, x0, y0, x1, y1) {
+  this._x = x;
+  this._y = y;
+  this._x0 = x0;
+  this._y0 = y0;
+  this._x1 = x1;
+  this._y1 = y1;
+  this._root = undefined;
+}
+
+function leaf_copy(leaf) {
+  var copy = {data: leaf.data}, next = copy;
+  while (leaf = leaf.next) next = next.next = {data: leaf.data};
+  return copy;
+}
+
+var treeProto = quadtree.prototype = Quadtree.prototype;
+
+treeProto.copy = function() {
+  var copy = new Quadtree(this._x, this._y, this._x0, this._y0, this._x1, this._y1),
+      node = this._root,
+      nodes,
+      child;
+
+  if (!node) return copy;
+
+  if (!node.length) return copy._root = leaf_copy(node), copy;
+
+  nodes = [{source: node, target: copy._root = new Array(4)}];
+  while (node = nodes.pop()) {
+    for (var i = 0; i < 4; ++i) {
+      if (child = node.source[i]) {
+        if (child.length) nodes.push({source: child, target: node.target[i] = new Array(4)});
+        else node.target[i] = leaf_copy(child);
+      }
+    }
+  }
+
+  return copy;
+};
+
+treeProto.add = tree_add;
+treeProto.addAll = addAll;
+treeProto.cover = tree_cover;
+treeProto.data = tree_data;
+treeProto.extent = tree_extent;
+treeProto.find = tree_find;
+treeProto.remove = tree_remove;
+treeProto.removeAll = removeAll;
+treeProto.root = tree_root;
+treeProto.size = tree_size;
+treeProto.visit = tree_visit;
+treeProto.visitAfter = tree_visitAfter;
+treeProto.x = tree_x;
+treeProto.y = tree_y;
+
+function x(d) {
+  return d.x + d.vx;
+}
+
+function y(d) {
+  return d.y + d.vy;
+}
+
+function collide(radius) {
+  var nodes,
+      radii,
+      strength = 1,
+      iterations = 1;
+
+  if (typeof radius !== "function") radius = constant$7(radius == null ? 1 : +radius);
+
+  function force() {
+    var i, n = nodes.length,
+        tree,
+        node,
+        xi,
+        yi,
+        ri,
+        ri2;
+
+    for (var k = 0; k < iterations; ++k) {
+      tree = quadtree(nodes, x, y).visitAfter(prepare);
+      for (i = 0; i < n; ++i) {
+        node = nodes[i];
+        ri = radii[node.index], ri2 = ri * ri;
+        xi = node.x + node.vx;
+        yi = node.y + node.vy;
+        tree.visit(apply);
+      }
+    }
+
+    function apply(quad, x0, y0, x1, y1) {
+      var data = quad.data, rj = quad.r, r = ri + rj;
+      if (data) {
+        if (data.index > node.index) {
+          var x = xi - data.x - data.vx,
+              y = yi - data.y - data.vy,
+              l = x * x + y * y;
+          if (l < r * r) {
+            if (x === 0) x = jiggle(), l += x * x;
+            if (y === 0) y = jiggle(), l += y * y;
+            l = (r - (l = Math.sqrt(l))) / l * strength;
+            node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
+            node.vy += (y *= l) * r;
+            data.vx -= x * (r = 1 - r);
+            data.vy -= y * r;
+          }
+        }
+        return;
+      }
+      return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
+    }
+  }
+
+  function prepare(quad) {
+    if (quad.data) return quad.r = radii[quad.data.index];
+    for (var i = quad.r = 0; i < 4; ++i) {
+      if (quad[i] && quad[i].r > quad.r) {
+        quad.r = quad[i].r;
+      }
+    }
+  }
+
+  function initialize() {
+    if (!nodes) return;
+    var i, n = nodes.length, node;
+    radii = new Array(n);
+    for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+    initialize();
+  };
+
+  force.iterations = function(_) {
+    return arguments.length ? (iterations = +_, force) : iterations;
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = +_, force) : strength;
+  };
+
+  force.radius = function(_) {
+    return arguments.length ? (radius = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : radius;
+  };
+
+  return force;
+}
+
+function index(d) {
+  return d.index;
+}
+
+function find(nodeById, nodeId) {
+  var node = nodeById.get(nodeId);
+  if (!node) throw new Error("missing: " + nodeId);
+  return node;
+}
+
+function link(links) {
+  var id = index,
+      strength = defaultStrength,
+      strengths,
+      distance = constant$7(30),
+      distances,
+      nodes,
+      count,
+      bias,
+      iterations = 1;
+
+  if (links == null) links = [];
+
+  function defaultStrength(link) {
+    return 1 / Math.min(count[link.source.index], count[link.target.index]);
+  }
+
+  function force(alpha) {
+    for (var k = 0, n = links.length; k < iterations; ++k) {
+      for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
+        link = links[i], source = link.source, target = link.target;
+        x = target.x + target.vx - source.x - source.vx || jiggle();
+        y = target.y + target.vy - source.y - source.vy || jiggle();
+        l = Math.sqrt(x * x + y * y);
+        l = (l - distances[i]) / l * alpha * strengths[i];
+        x *= l, y *= l;
+        target.vx -= x * (b = bias[i]);
+        target.vy -= y * b;
+        source.vx += x * (b = 1 - b);
+        source.vy += y * b;
+      }
+    }
+  }
+
+  function initialize() {
+    if (!nodes) return;
+
+    var i,
+        n = nodes.length,
+        m = links.length,
+        nodeById = map$1(nodes, id),
+        link;
+
+    for (i = 0, count = new Array(n); i < m; ++i) {
+      link = links[i], link.index = i;
+      if (typeof link.source !== "object") link.source = find(nodeById, link.source);
+      if (typeof link.target !== "object") link.target = find(nodeById, link.target);
+      count[link.source.index] = (count[link.source.index] || 0) + 1;
+      count[link.target.index] = (count[link.target.index] || 0) + 1;
+    }
+
+    for (i = 0, bias = new Array(m); i < m; ++i) {
+      link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
+    }
+
+    strengths = new Array(m), initializeStrength();
+    distances = new Array(m), initializeDistance();
+  }
+
+  function initializeStrength() {
+    if (!nodes) return;
+
+    for (var i = 0, n = links.length; i < n; ++i) {
+      strengths[i] = +strength(links[i], i, links);
+    }
+  }
+
+  function initializeDistance() {
+    if (!nodes) return;
+
+    for (var i = 0, n = links.length; i < n; ++i) {
+      distances[i] = +distance(links[i], i, links);
+    }
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+    initialize();
+  };
+
+  force.links = function(_) {
+    return arguments.length ? (links = _, initialize(), force) : links;
+  };
+
+  force.id = function(_) {
+    return arguments.length ? (id = _, force) : id;
+  };
+
+  force.iterations = function(_) {
+    return arguments.length ? (iterations = +_, force) : iterations;
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = typeof _ === "function" ? _ : constant$7(+_), initializeStrength(), force) : strength;
+  };
+
+  force.distance = function(_) {
+    return arguments.length ? (distance = typeof _ === "function" ? _ : constant$7(+_), initializeDistance(), force) : distance;
+  };
+
+  return force;
+}
+
+function x$1(d) {
+  return d.x;
+}
+
+function y$1(d) {
+  return d.y;
+}
+
+var initialRadius = 10,
+    initialAngle = Math.PI * (3 - Math.sqrt(5));
+
+function simulation(nodes) {
+  var simulation,
+      alpha = 1,
+      alphaMin = 0.001,
+      alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
+      alphaTarget = 0,
+      velocityDecay = 0.6,
+      forces = map$1(),
+      stepper = timer(step),
+      event = dispatch("tick", "end");
+
+  if (nodes == null) nodes = [];
+
+  function step() {
+    tick();
+    event.call("tick", simulation);
+    if (alpha < alphaMin) {
+      stepper.stop();
+      event.call("end", simulation);
+    }
+  }
+
+  function tick(iterations) {
+    var i, n = nodes.length, node;
+
+    if (iterations === undefined) iterations = 1;
+
+    for (var k = 0; k < iterations; ++k) {
+      alpha += (alphaTarget - alpha) * alphaDecay;
+
+      forces.each(function (force) {
+        force(alpha);
+      });
+
+      for (i = 0; i < n; ++i) {
+        node = nodes[i];
+        if (node.fx == null) node.x += node.vx *= velocityDecay;
+        else node.x = node.fx, node.vx = 0;
+        if (node.fy == null) node.y += node.vy *= velocityDecay;
+        else node.y = node.fy, node.vy = 0;
+      }
+    }
+
+    return simulation;
+  }
+
+  function initializeNodes() {
+    for (var i = 0, n = nodes.length, node; i < n; ++i) {
+      node = nodes[i], node.index = i;
+      if (node.fx != null) node.x = node.fx;
+      if (node.fy != null) node.y = node.fy;
+      if (isNaN(node.x) || isNaN(node.y)) {
+        var radius = initialRadius * Math.sqrt(i), angle = i * initialAngle;
+        node.x = radius * Math.cos(angle);
+        node.y = radius * Math.sin(angle);
+      }
+      if (isNaN(node.vx) || isNaN(node.vy)) {
+        node.vx = node.vy = 0;
+      }
+    }
+  }
+
+  function initializeForce(force) {
+    if (force.initialize) force.initialize(nodes);
+    return force;
+  }
+
+  initializeNodes();
+
+  return simulation = {
+    tick: tick,
+
+    restart: function() {
+      return stepper.restart(step), simulation;
+    },
+
+    stop: function() {
+      return stepper.stop(), simulation;
+    },
+
+    nodes: function(_) {
+      return arguments.length ? (nodes = _, initializeNodes(), forces.each(initializeForce), simulation) : nodes;
+    },
+
+    alpha: function(_) {
+      return arguments.length ? (alpha = +_, simulation) : alpha;
+    },
+
+    alphaMin: function(_) {
+      return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
+    },
+
+    alphaDecay: function(_) {
+      return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
+    },
+
+    alphaTarget: function(_) {
+      return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
+    },
+
+    velocityDecay: function(_) {
+      return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
+    },
+
+    force: function(name, _) {
+      return arguments.length > 1 ? ((_ == null ? forces.remove(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
+    },
+
+    find: function(x, y, radius) {
+      var i = 0,
+          n = nodes.length,
+          dx,
+          dy,
+          d2,
+          node,
+          closest;
+
+      if (radius == null) radius = Infinity;
+      else radius *= radius;
+
+      for (i = 0; i < n; ++i) {
+        node = nodes[i];
+        dx = x - node.x;
+        dy = y - node.y;
+        d2 = dx * dx + dy * dy;
+        if (d2 < radius) closest = node, radius = d2;
+      }
+
+      return closest;
+    },
+
+    on: function(name, _) {
+      return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
+    }
+  };
+}
+
+function manyBody() {
+  var nodes,
+      node,
+      alpha,
+      strength = constant$7(-30),
+      strengths,
+      distanceMin2 = 1,
+      distanceMax2 = Infinity,
+      theta2 = 0.81;
+
+  function force(_) {
+    var i, n = nodes.length, tree = quadtree(nodes, x$1, y$1).visitAfter(accumulate);
+    for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
+  }
+
+  function initialize() {
+    if (!nodes) return;
+    var i, n = nodes.length, node;
+    strengths = new Array(n);
+    for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes);
+  }
+
+  function accumulate(quad) {
+    var strength = 0, q, c, weight = 0, x, y, i;
+
+    // For internal nodes, accumulate forces from child quadrants.
+    if (quad.length) {
+      for (x = y = i = 0; i < 4; ++i) {
+        if ((q = quad[i]) && (c = Math.abs(q.value))) {
+          strength += q.value, weight += c, x += c * q.x, y += c * q.y;
+        }
+      }
+      quad.x = x / weight;
+      quad.y = y / weight;
+    }
+
+    // For leaf nodes, accumulate forces from coincident quadrants.
+    else {
+      q = quad;
+      q.x = q.data.x;
+      q.y = q.data.y;
+      do strength += strengths[q.data.index];
+      while (q = q.next);
+    }
+
+    quad.value = strength;
+  }
+
+  function apply(quad, x1, _, x2) {
+    if (!quad.value) return true;
+
+    var x = quad.x - node.x,
+        y = quad.y - node.y,
+        w = x2 - x1,
+        l = x * x + y * y;
+
+    // Apply the Barnes-Hut approximation if possible.
+    // Limit forces for very close nodes; randomize direction if coincident.
+    if (w * w / theta2 < l) {
+      if (l < distanceMax2) {
+        if (x === 0) x = jiggle(), l += x * x;
+        if (y === 0) y = jiggle(), l += y * y;
+        if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
+        node.vx += x * quad.value * alpha / l;
+        node.vy += y * quad.value * alpha / l;
+      }
+      return true;
+    }
+
+    // Otherwise, process points directly.
+    else if (quad.length || l >= distanceMax2) return;
+
+    // Limit forces for very close nodes; randomize direction if coincident.
+    if (quad.data !== node || quad.next) {
+      if (x === 0) x = jiggle(), l += x * x;
+      if (y === 0) y = jiggle(), l += y * y;
+      if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
+    }
+
+    do if (quad.data !== node) {
+      w = strengths[quad.data.index] * alpha / l;
+      node.vx += x * w;
+      node.vy += y * w;
+    } while (quad = quad.next);
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+    initialize();
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : strength;
+  };
+
+  force.distanceMin = function(_) {
+    return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
+  };
+
+  force.distanceMax = function(_) {
+    return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
+  };
+
+  force.theta = function(_) {
+    return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
+  };
+
+  return force;
+}
+
+function radial(radius, x, y) {
+  var nodes,
+      strength = constant$7(0.1),
+      strengths,
+      radiuses;
+
+  if (typeof radius !== "function") radius = constant$7(+radius);
+  if (x == null) x = 0;
+  if (y == null) y = 0;
+
+  function force(alpha) {
+    for (var i = 0, n = nodes.length; i < n; ++i) {
+      var node = nodes[i],
+          dx = node.x - x || 1e-6,
+          dy = node.y - y || 1e-6,
+          r = Math.sqrt(dx * dx + dy * dy),
+          k = (radiuses[i] - r) * strengths[i] * alpha / r;
+      node.vx += dx * k;
+      node.vy += dy * k;
+    }
+  }
+
+  function initialize() {
+    if (!nodes) return;
+    var i, n = nodes.length;
+    strengths = new Array(n);
+    radiuses = new Array(n);
+    for (i = 0; i < n; ++i) {
+      radiuses[i] = +radius(nodes[i], i, nodes);
+      strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes);
+    }
+  }
+
+  force.initialize = function(_) {
+    nodes = _, initialize();
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : strength;
+  };
+
+  force.radius = function(_) {
+    return arguments.length ? (radius = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : radius;
+  };
+
+  force.x = function(_) {
+    return arguments.length ? (x = +_, force) : x;
+  };
+
+  force.y = function(_) {
+    return arguments.length ? (y = +_, force) : y;
+  };
+
+  return force;
+}
+
+function x$2(x) {
+  var strength = constant$7(0.1),
+      nodes,
+      strengths,
+      xz;
+
+  if (typeof x !== "function") x = constant$7(x == null ? 0 : +x);
+
+  function force(alpha) {
+    for (var i = 0, n = nodes.length, node; i < n; ++i) {
+      node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
+    }
+  }
+
+  function initialize() {
+    if (!nodes) return;
+    var i, n = nodes.length;
+    strengths = new Array(n);
+    xz = new Array(n);
+    for (i = 0; i < n; ++i) {
+      strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
+    }
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+    initialize();
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : strength;
+  };
+
+  force.x = function(_) {
+    return arguments.length ? (x = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : x;
+  };
+
+  return force;
+}
+
+function y$2(y) {
+  var strength = constant$7(0.1),
+      nodes,
+      strengths,
+      yz;
+
+  if (typeof y !== "function") y = constant$7(y == null ? 0 : +y);
+
+  function force(alpha) {
+    for (var i = 0, n = nodes.length, node; i < n; ++i) {
+      node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
+    }
+  }
+
+  function initialize() {
+    if (!nodes) return;
+    var i, n = nodes.length;
+    strengths = new Array(n);
+    yz = new Array(n);
+    for (i = 0; i < n; ++i) {
+      strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
+    }
+  }
+
+  force.initialize = function(_) {
+    nodes = _;
+    initialize();
+  };
+
+  force.strength = function(_) {
+    return arguments.length ? (strength = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : strength;
+  };
+
+  force.y = function(_) {
+    return arguments.length ? (y = typeof _ === "function" ? _ : constant$7(+_), initialize(), force) : y;
+  };
+
+  return force;
+}
+
+// Computes the decimal coefficient and exponent of the specified number x with
+// significant digits p, where x is positive and p is in [1, 21] or undefined.
+// For example, formatDecimal(1.23) returns ["123", 0].
+function formatDecimal(x, p) {
+  if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity
+  var i, coefficient = x.slice(0, i);
+
+  // The string returned by toExponential either has the form \d\.\d+e[-+]\d+
+  // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3).
+  return [
+    coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient,
+    +x.slice(i + 1)
+  ];
+}
+
+function exponent$1(x) {
+  return x = formatDecimal(Math.abs(x)), x ? x[1] : NaN;
+}
+
+function formatGroup(grouping, thousands) {
+  return function(value, width) {
+    var i = value.length,
+        t = [],
+        j = 0,
+        g = grouping[0],
+        length = 0;
+
+    while (i > 0 && g > 0) {
+      if (length + g + 1 > width) g = Math.max(1, width - length);
+      t.push(value.substring(i -= g, i + g));
+      if ((length += g + 1) > width) break;
+      g = grouping[j = (j + 1) % grouping.length];
+    }
+
+    return t.reverse().join(thousands);
+  };
+}
+
+function formatNumerals(numerals) {
+  return function(value) {
+    return value.replace(/[0-9]/g, function(i) {
+      return numerals[+i];
+    });
+  };
+}
+
+// [[fill]align][sign][symbol][0][width][,][.precision][~][type]
+var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;
+
+function formatSpecifier(specifier) {
+  return new FormatSpecifier(specifier);
+}
+
+formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof
+
+function FormatSpecifier(specifier) {
+  if (!(match = re.exec(specifier))) throw new Error("invalid format: " + specifier);
+  var match;
+  this.fill = match[1] || " ";
+  this.align = match[2] || ">";
+  this.sign = match[3] || "-";
+  this.symbol = match[4] || "";
+  this.zero = !!match[5];
+  this.width = match[6] && +match[6];
+  this.comma = !!match[7];
+  this.precision = match[8] && +match[8].slice(1);
+  this.trim = !!match[9];
+  this.type = match[10] || "";
+}
+
+FormatSpecifier.prototype.toString = function() {
+  return this.fill
+      + this.align
+      + this.sign
+      + this.symbol
+      + (this.zero ? "0" : "")
+      + (this.width == null ? "" : Math.max(1, this.width | 0))
+      + (this.comma ? "," : "")
+      + (this.precision == null ? "" : "." + Math.max(0, this.precision | 0))
+      + (this.trim ? "~" : "")
+      + this.type;
+};
+
+// Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.
+function formatTrim(s) {
+  out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {
+    switch (s[i]) {
+      case ".": i0 = i1 = i; break;
+      case "0": if (i0 === 0) i0 = i; i1 = i; break;
+      default: if (i0 > 0) { if (!+s[i]) break out; i0 = 0; } break;
+    }
+  }
+  return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;
+}
+
+var prefixExponent;
+
+function formatPrefixAuto(x, p) {
+  var d = formatDecimal(x, p);
+  if (!d) return x + "";
+  var coefficient = d[0],
+      exponent = d[1],
+      i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,
+      n = coefficient.length;
+  return i === n ? coefficient
+      : i > n ? coefficient + new Array(i - n + 1).join("0")
+      : i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i)
+      : "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than 1y!
+}
+
+function formatRounded(x, p) {
+  var d = formatDecimal(x, p);
+  if (!d) return x + "";
+  var coefficient = d[0],
+      exponent = d[1];
+  return exponent < 0 ? "0." + new Array(-exponent).join("0") + coefficient
+      : coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + "." + coefficient.slice(exponent + 1)
+      : coefficient + new Array(exponent - coefficient.length + 2).join("0");
+}
+
+var formatTypes = {
+  "%": function(x, p) { return (x * 100).toFixed(p); },
+  "b": function(x) { return Math.round(x).toString(2); },
+  "c": function(x) { return x + ""; },
+  "d": function(x) { return Math.round(x).toString(10); },
+  "e": function(x, p) { return x.toExponential(p); },
+  "f": function(x, p) { return x.toFixed(p); },
+  "g": function(x, p) { return x.toPrecision(p); },
+  "o": function(x) { return Math.round(x).toString(8); },
+  "p": function(x, p) { return formatRounded(x * 100, p); },
+  "r": formatRounded,
+  "s": formatPrefixAuto,
+  "X": function(x) { return Math.round(x).toString(16).toUpperCase(); },
+  "x": function(x) { return Math.round(x).toString(16); }
+};
+
+function identity$3(x) {
+  return x;
+}
+
+var prefixes = ["y","z","a","f","p","n","\xB5","m","","k","M","G","T","P","E","Z","Y"];
+
+function formatLocale(locale) {
+  var group = locale.grouping && locale.thousands ? formatGroup(locale.grouping, locale.thousands) : identity$3,
+      currency = locale.currency,
+      decimal = locale.decimal,
+      numerals = locale.numerals ? formatNumerals(locale.numerals) : identity$3,
+      percent = locale.percent || "%";
+
+  function newFormat(specifier) {
+    specifier = formatSpecifier(specifier);
+
+    var fill = specifier.fill,
+        align = specifier.align,
+        sign = specifier.sign,
+        symbol = specifier.symbol,
+        zero = specifier.zero,
+        width = specifier.width,
+        comma = specifier.comma,
+        precision = specifier.precision,
+        trim = specifier.trim,
+        type = specifier.type;
+
+    // The "n" type is an alias for ",g".
+    if (type === "n") comma = true, type = "g";
+
+    // The "" type, and any invalid type, is an alias for ".12~g".
+    else if (!formatTypes[type]) precision == null && (precision = 12), trim = true, type = "g";
+
+    // If zero fill is specified, padding goes after sign and before digits.
+    if (zero || (fill === "0" && align === "=")) zero = true, fill = "0", align = "=";
+
+    // Compute the prefix and suffix.
+    // For SI-prefix, the suffix is lazily computed.
+    var prefix = symbol === "$" ? currency[0] : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "",
+        suffix = symbol === "$" ? currency[1] : /[%p]/.test(type) ? percent : "";
+
+    // What format function should we use?
+    // Is this an integer type?
+    // Can this type generate exponential notation?
+    var formatType = formatTypes[type],
+        maybeSuffix = /[defgprs%]/.test(type);
+
+    // Set the default precision if not specified,
+    // or clamp the specified precision to the supported range.
+    // For significant precision, it must be in [1, 21].
+    // For fixed precision, it must be in [0, 20].
+    precision = precision == null ? 6
+        : /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))
+        : Math.max(0, Math.min(20, precision));
+
+    function format(value) {
+      var valuePrefix = prefix,
+          valueSuffix = suffix,
+          i, n, c;
+
+      if (type === "c") {
+        valueSuffix = formatType(value) + valueSuffix;
+        value = "";
+      } else {
+        value = +value;
+
+        // Perform the initial formatting.
+        var valueNegative = value < 0;
+        value = formatType(Math.abs(value), precision);
+
+        // Trim insignificant zeros.
+        if (trim) value = formatTrim(value);
+
+        // If a negative value rounds to zero during formatting, treat as positive.
+        if (valueNegative && +value === 0) valueNegative = false;
+
+        // Compute the prefix and suffix.
+        valuePrefix = (valueNegative ? (sign === "(" ? sign : "-") : sign === "-" || sign === "(" ? "" : sign) + valuePrefix;
+        valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : "");
+
+        // Break the formatted value into the integer “value” part that can be
+        // grouped, and fractional or exponential “suffix” part that is not.
+        if (maybeSuffix) {
+          i = -1, n = value.length;
+          while (++i < n) {
+            if (c = value.charCodeAt(i), 48 > c || c > 57) {
+              valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix;
+              value = value.slice(0, i);
+              break;
+            }
+          }
+        }
+      }
+
+      // If the fill character is not "0", grouping is applied before padding.
+      if (comma && !zero) value = group(value, Infinity);
+
+      // Compute the padding.
+      var length = valuePrefix.length + value.length + valueSuffix.length,
+          padding = length < width ? new Array(width - length + 1).join(fill) : "";
+
+      // If the fill character is "0", grouping is applied after padding.
+      if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = "";
+
+      // Reconstruct the final output based on the desired alignment.
+      switch (align) {
+        case "<": value = valuePrefix + value + valueSuffix + padding; break;
+        case "=": value = valuePrefix + padding + value + valueSuffix; break;
+        case "^": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break;
+        default: value = padding + valuePrefix + value + valueSuffix; break;
+      }
+
+      return numerals(value);
+    }
+
+    format.toString = function() {
+      return specifier + "";
+    };
+
+    return format;
+  }
+
+  function formatPrefix(specifier, value) {
+    var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
+        e = Math.max(-8, Math.min(8, Math.floor(exponent$1(value) / 3))) * 3,
+        k = Math.pow(10, -e),
+        prefix = prefixes[8 + e / 3];
+    return function(value) {
+      return f(k * value) + prefix;
+    };
+  }
+
+  return {
+    format: newFormat,
+    formatPrefix: formatPrefix
+  };
+}
+
+var locale;
+
+defaultLocale({
+  decimal: ".",
+  thousands: ",",
+  grouping: [3],
+  currency: ["$", ""]
+});
+
+function defaultLocale(definition) {
+  locale = formatLocale(definition);
+  exports.format = locale.format;
+  exports.formatPrefix = locale.formatPrefix;
+  return locale;
+}
+
+function precisionFixed(step) {
+  return Math.max(0, -exponent$1(Math.abs(step)));
+}
+
+function precisionPrefix(step, value) {
+  return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent$1(value) / 3))) * 3 - exponent$1(Math.abs(step)));
+}
+
+function precisionRound(step, max) {
+  step = Math.abs(step), max = Math.abs(max) - step;
+  return Math.max(0, exponent$1(max) - exponent$1(step)) + 1;
+}
+
+// Adds floating point numbers with twice the normal precision.
+// Reference: J. R. Shewchuk, Adaptive Precision Floating-Point Arithmetic and
+// Fast Robust Geometric Predicates, Discrete & Computational Geometry 18(3)
+// 305–363 (1997).
+// Code adapted from GeographicLib by Charles F. F. Karney,
+// http://geographiclib.sourceforge.net/
+
+function adder() {
+  return new Adder;
+}
+
+function Adder() {
+  this.reset();
+}
+
+Adder.prototype = {
+  constructor: Adder,
+  reset: function() {
+    this.s = // rounded value
+    this.t = 0; // exact error
+  },
+  add: function(y) {
+    add$1(temp, y, this.t);
+    add$1(this, temp.s, this.s);
+    if (this.s) this.t += temp.t;
+    else this.s = temp.t;
+  },
+  valueOf: function() {
+    return this.s;
+  }
+};
+
+var temp = new Adder;
+
+function add$1(adder, a, b) {
+  var x = adder.s = a + b,
+      bv = x - a,
+      av = x - bv;
+  adder.t = (a - av) + (b - bv);
+}
+
+var epsilon$2 = 1e-6;
+var epsilon2$1 = 1e-12;
+var pi$3 = Math.PI;
+var halfPi$2 = pi$3 / 2;
+var quarterPi = pi$3 / 4;
+var tau$3 = pi$3 * 2;
+
+var degrees$1 = 180 / pi$3;
+var radians = pi$3 / 180;
+
+var abs = Math.abs;
+var atan = Math.atan;
+var atan2 = Math.atan2;
+var cos$1 = Math.cos;
+var ceil = Math.ceil;
+var exp = Math.exp;
+var log = Math.log;
+var pow = Math.pow;
+var sin$1 = Math.sin;
+var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; };
+var sqrt = Math.sqrt;
+var tan = Math.tan;
+
+function acos(x) {
+  return x > 1 ? 0 : x < -1 ? pi$3 : Math.acos(x);
+}
+
+function asin(x) {
+  return x > 1 ? halfPi$2 : x < -1 ? -halfPi$2 : Math.asin(x);
+}
+
+function haversin(x) {
+  return (x = sin$1(x / 2)) * x;
+}
+
+function noop$2() {}
+
+function streamGeometry(geometry, stream) {
+  if (geometry && streamGeometryType.hasOwnProperty(geometry.type)) {
+    streamGeometryType[geometry.type](geometry, stream);
+  }
+}
+
+var streamObjectType = {
+  Feature: function(object, stream) {
+    streamGeometry(object.geometry, stream);
+  },
+  FeatureCollection: function(object, stream) {
+    var features = object.features, i = -1, n = features.length;
+    while (++i < n) streamGeometry(features[i].geometry, stream);
+  }
+};
+
+var streamGeometryType = {
+  Sphere: function(object, stream) {
+    stream.sphere();
+  },
+  Point: function(object, stream) {
+    object = object.coordinates;
+    stream.point(object[0], object[1], object[2]);
+  },
+  MultiPoint: function(object, stream) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) object = coordinates[i], stream.point(object[0], object[1], object[2]);
+  },
+  LineString: function(object, stream) {
+    streamLine(object.coordinates, stream, 0);
+  },
+  MultiLineString: function(object, stream) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) streamLine(coordinates[i], stream, 0);
+  },
+  Polygon: function(object, stream) {
+    streamPolygon(object.coordinates, stream);
+  },
+  MultiPolygon: function(object, stream) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) streamPolygon(coordinates[i], stream);
+  },
+  GeometryCollection: function(object, stream) {
+    var geometries = object.geometries, i = -1, n = geometries.length;
+    while (++i < n) streamGeometry(geometries[i], stream);
+  }
+};
+
+function streamLine(coordinates, stream, closed) {
+  var i = -1, n = coordinates.length - closed, coordinate;
+  stream.lineStart();
+  while (++i < n) coordinate = coordinates[i], stream.point(coordinate[0], coordinate[1], coordinate[2]);
+  stream.lineEnd();
+}
+
+function streamPolygon(coordinates, stream) {
+  var i = -1, n = coordinates.length;
+  stream.polygonStart();
+  while (++i < n) streamLine(coordinates[i], stream, 1);
+  stream.polygonEnd();
+}
+
+function geoStream(object, stream) {
+  if (object && streamObjectType.hasOwnProperty(object.type)) {
+    streamObjectType[object.type](object, stream);
+  } else {
+    streamGeometry(object, stream);
+  }
+}
+
+var areaRingSum = adder();
+
+var areaSum = adder(),
+    lambda00,
+    phi00,
+    lambda0,
+    cosPhi0,
+    sinPhi0;
+
+var areaStream = {
+  point: noop$2,
+  lineStart: noop$2,
+  lineEnd: noop$2,
+  polygonStart: function() {
+    areaRingSum.reset();
+    areaStream.lineStart = areaRingStart;
+    areaStream.lineEnd = areaRingEnd;
+  },
+  polygonEnd: function() {
+    var areaRing = +areaRingSum;
+    areaSum.add(areaRing < 0 ? tau$3 + areaRing : areaRing);
+    this.lineStart = this.lineEnd = this.point = noop$2;
+  },
+  sphere: function() {
+    areaSum.add(tau$3);
+  }
+};
+
+function areaRingStart() {
+  areaStream.point = areaPointFirst;
+}
+
+function areaRingEnd() {
+  areaPoint(lambda00, phi00);
+}
+
+function areaPointFirst(lambda, phi) {
+  areaStream.point = areaPoint;
+  lambda00 = lambda, phi00 = phi;
+  lambda *= radians, phi *= radians;
+  lambda0 = lambda, cosPhi0 = cos$1(phi = phi / 2 + quarterPi), sinPhi0 = sin$1(phi);
+}
+
+function areaPoint(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  phi = phi / 2 + quarterPi; // half the angular distance from south pole
+
+  // Spherical excess E for a spherical triangle with vertices: south pole,
+  // previous point, current point.  Uses a formula derived from Cagnoli’s
+  // theorem.  See Todhunter, Spherical Trig. (1871), Sec. 103, Eq. (2).
+  var dLambda = lambda - lambda0,
+      sdLambda = dLambda >= 0 ? 1 : -1,
+      adLambda = sdLambda * dLambda,
+      cosPhi = cos$1(phi),
+      sinPhi = sin$1(phi),
+      k = sinPhi0 * sinPhi,
+      u = cosPhi0 * cosPhi + k * cos$1(adLambda),
+      v = k * sdLambda * sin$1(adLambda);
+  areaRingSum.add(atan2(v, u));
+
+  // Advance the previous points.
+  lambda0 = lambda, cosPhi0 = cosPhi, sinPhi0 = sinPhi;
+}
+
+function area$1(object) {
+  areaSum.reset();
+  geoStream(object, areaStream);
+  return areaSum * 2;
+}
+
+function spherical(cartesian) {
+  return [atan2(cartesian[1], cartesian[0]), asin(cartesian[2])];
+}
+
+function cartesian(spherical) {
+  var lambda = spherical[0], phi = spherical[1], cosPhi = cos$1(phi);
+  return [cosPhi * cos$1(lambda), cosPhi * sin$1(lambda), sin$1(phi)];
+}
+
+function cartesianDot(a, b) {
+  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+}
+
+function cartesianCross(a, b) {
+  return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
+}
+
+// TODO return a
+function cartesianAddInPlace(a, b) {
+  a[0] += b[0], a[1] += b[1], a[2] += b[2];
+}
+
+function cartesianScale(vector, k) {
+  return [vector[0] * k, vector[1] * k, vector[2] * k];
+}
+
+// TODO return d
+function cartesianNormalizeInPlace(d) {
+  var l = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
+  d[0] /= l, d[1] /= l, d[2] /= l;
+}
+
+var lambda0$1, phi0, lambda1, phi1, // bounds
+    lambda2, // previous lambda-coordinate
+    lambda00$1, phi00$1, // first point
+    p0, // previous 3D point
+    deltaSum = adder(),
+    ranges,
+    range;
+
+var boundsStream = {
+  point: boundsPoint,
+  lineStart: boundsLineStart,
+  lineEnd: boundsLineEnd,
+  polygonStart: function() {
+    boundsStream.point = boundsRingPoint;
+    boundsStream.lineStart = boundsRingStart;
+    boundsStream.lineEnd = boundsRingEnd;
+    deltaSum.reset();
+    areaStream.polygonStart();
+  },
+  polygonEnd: function() {
+    areaStream.polygonEnd();
+    boundsStream.point = boundsPoint;
+    boundsStream.lineStart = boundsLineStart;
+    boundsStream.lineEnd = boundsLineEnd;
+    if (areaRingSum < 0) lambda0$1 = -(lambda1 = 180), phi0 = -(phi1 = 90);
+    else if (deltaSum > epsilon$2) phi1 = 90;
+    else if (deltaSum < -epsilon$2) phi0 = -90;
+    range[0] = lambda0$1, range[1] = lambda1;
+  }
+};
+
+function boundsPoint(lambda, phi) {
+  ranges.push(range = [lambda0$1 = lambda, lambda1 = lambda]);
+  if (phi < phi0) phi0 = phi;
+  if (phi > phi1) phi1 = phi;
+}
+
+function linePoint(lambda, phi) {
+  var p = cartesian([lambda * radians, phi * radians]);
+  if (p0) {
+    var normal = cartesianCross(p0, p),
+        equatorial = [normal[1], -normal[0], 0],
+        inflection = cartesianCross(equatorial, normal);
+    cartesianNormalizeInPlace(inflection);
+    inflection = spherical(inflection);
+    var delta = lambda - lambda2,
+        sign$$1 = delta > 0 ? 1 : -1,
+        lambdai = inflection[0] * degrees$1 * sign$$1,
+        phii,
+        antimeridian = abs(delta) > 180;
+    if (antimeridian ^ (sign$$1 * lambda2 < lambdai && lambdai < sign$$1 * lambda)) {
+      phii = inflection[1] * degrees$1;
+      if (phii > phi1) phi1 = phii;
+    } else if (lambdai = (lambdai + 360) % 360 - 180, antimeridian ^ (sign$$1 * lambda2 < lambdai && lambdai < sign$$1 * lambda)) {
+      phii = -inflection[1] * degrees$1;
+      if (phii < phi0) phi0 = phii;
+    } else {
+      if (phi < phi0) phi0 = phi;
+      if (phi > phi1) phi1 = phi;
+    }
+    if (antimeridian) {
+      if (lambda < lambda2) {
+        if (angle(lambda0$1, lambda) > angle(lambda0$1, lambda1)) lambda1 = lambda;
+      } else {
+        if (angle(lambda, lambda1) > angle(lambda0$1, lambda1)) lambda0$1 = lambda;
+      }
+    } else {
+      if (lambda1 >= lambda0$1) {
+        if (lambda < lambda0$1) lambda0$1 = lambda;
+        if (lambda > lambda1) lambda1 = lambda;
+      } else {
+        if (lambda > lambda2) {
+          if (angle(lambda0$1, lambda) > angle(lambda0$1, lambda1)) lambda1 = lambda;
+        } else {
+          if (angle(lambda, lambda1) > angle(lambda0$1, lambda1)) lambda0$1 = lambda;
+        }
+      }
+    }
+  } else {
+    ranges.push(range = [lambda0$1 = lambda, lambda1 = lambda]);
+  }
+  if (phi < phi0) phi0 = phi;
+  if (phi > phi1) phi1 = phi;
+  p0 = p, lambda2 = lambda;
+}
+
+function boundsLineStart() {
+  boundsStream.point = linePoint;
+}
+
+function boundsLineEnd() {
+  range[0] = lambda0$1, range[1] = lambda1;
+  boundsStream.point = boundsPoint;
+  p0 = null;
+}
+
+function boundsRingPoint(lambda, phi) {
+  if (p0) {
+    var delta = lambda - lambda2;
+    deltaSum.add(abs(delta) > 180 ? delta + (delta > 0 ? 360 : -360) : delta);
+  } else {
+    lambda00$1 = lambda, phi00$1 = phi;
+  }
+  areaStream.point(lambda, phi);
+  linePoint(lambda, phi);
+}
+
+function boundsRingStart() {
+  areaStream.lineStart();
+}
+
+function boundsRingEnd() {
+  boundsRingPoint(lambda00$1, phi00$1);
+  areaStream.lineEnd();
+  if (abs(deltaSum) > epsilon$2) lambda0$1 = -(lambda1 = 180);
+  range[0] = lambda0$1, range[1] = lambda1;
+  p0 = null;
+}
+
+// Finds the left-right distance between two longitudes.
+// This is almost the same as (lambda1 - lambda0 + 360°) % 360°, except that we want
+// the distance between ±180° to be 360°.
+function angle(lambda0, lambda1) {
+  return (lambda1 -= lambda0) < 0 ? lambda1 + 360 : lambda1;
+}
+
+function rangeCompare(a, b) {
+  return a[0] - b[0];
+}
+
+function rangeContains(range, x) {
+  return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;
+}
+
+function bounds(feature) {
+  var i, n, a, b, merged, deltaMax, delta;
+
+  phi1 = lambda1 = -(lambda0$1 = phi0 = Infinity);
+  ranges = [];
+  geoStream(feature, boundsStream);
+
+  // First, sort ranges by their minimum longitudes.
+  if (n = ranges.length) {
+    ranges.sort(rangeCompare);
+
+    // Then, merge any ranges that overlap.
+    for (i = 1, a = ranges[0], merged = [a]; i < n; ++i) {
+      b = ranges[i];
+      if (rangeContains(a, b[0]) || rangeContains(a, b[1])) {
+        if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];
+        if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];
+      } else {
+        merged.push(a = b);
+      }
+    }
+
+    // Finally, find the largest gap between the merged ranges.
+    // The final bounding box will be the inverse of this gap.
+    for (deltaMax = -Infinity, n = merged.length - 1, i = 0, a = merged[n]; i <= n; a = b, ++i) {
+      b = merged[i];
+      if ((delta = angle(a[1], b[0])) > deltaMax) deltaMax = delta, lambda0$1 = b[0], lambda1 = a[1];
+    }
+  }
+
+  ranges = range = null;
+
+  return lambda0$1 === Infinity || phi0 === Infinity
+      ? [[NaN, NaN], [NaN, NaN]]
+      : [[lambda0$1, phi0], [lambda1, phi1]];
+}
+
+var W0, W1,
+    X0, Y0, Z0,
+    X1, Y1, Z1,
+    X2, Y2, Z2,
+    lambda00$2, phi00$2, // first point
+    x0, y0, z0; // previous point
+
+var centroidStream = {
+  sphere: noop$2,
+  point: centroidPoint,
+  lineStart: centroidLineStart,
+  lineEnd: centroidLineEnd,
+  polygonStart: function() {
+    centroidStream.lineStart = centroidRingStart;
+    centroidStream.lineEnd = centroidRingEnd;
+  },
+  polygonEnd: function() {
+    centroidStream.lineStart = centroidLineStart;
+    centroidStream.lineEnd = centroidLineEnd;
+  }
+};
+
+// Arithmetic mean of Cartesian vectors.
+function centroidPoint(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  var cosPhi = cos$1(phi);
+  centroidPointCartesian(cosPhi * cos$1(lambda), cosPhi * sin$1(lambda), sin$1(phi));
+}
+
+function centroidPointCartesian(x, y, z) {
+  ++W0;
+  X0 += (x - X0) / W0;
+  Y0 += (y - Y0) / W0;
+  Z0 += (z - Z0) / W0;
+}
+
+function centroidLineStart() {
+  centroidStream.point = centroidLinePointFirst;
+}
+
+function centroidLinePointFirst(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  var cosPhi = cos$1(phi);
+  x0 = cosPhi * cos$1(lambda);
+  y0 = cosPhi * sin$1(lambda);
+  z0 = sin$1(phi);
+  centroidStream.point = centroidLinePoint;
+  centroidPointCartesian(x0, y0, z0);
+}
+
+function centroidLinePoint(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  var cosPhi = cos$1(phi),
+      x = cosPhi * cos$1(lambda),
+      y = cosPhi * sin$1(lambda),
+      z = sin$1(phi),
+      w = atan2(sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);
+  W1 += w;
+  X1 += w * (x0 + (x0 = x));
+  Y1 += w * (y0 + (y0 = y));
+  Z1 += w * (z0 + (z0 = z));
+  centroidPointCartesian(x0, y0, z0);
+}
+
+function centroidLineEnd() {
+  centroidStream.point = centroidPoint;
+}
+
+// See J. E. Brock, The Inertia Tensor for a Spherical Triangle,
+// J. Applied Mechanics 42, 239 (1975).
+function centroidRingStart() {
+  centroidStream.point = centroidRingPointFirst;
+}
+
+function centroidRingEnd() {
+  centroidRingPoint(lambda00$2, phi00$2);
+  centroidStream.point = centroidPoint;
+}
+
+function centroidRingPointFirst(lambda, phi) {
+  lambda00$2 = lambda, phi00$2 = phi;
+  lambda *= radians, phi *= radians;
+  centroidStream.point = centroidRingPoint;
+  var cosPhi = cos$1(phi);
+  x0 = cosPhi * cos$1(lambda);
+  y0 = cosPhi * sin$1(lambda);
+  z0 = sin$1(phi);
+  centroidPointCartesian(x0, y0, z0);
+}
+
+function centroidRingPoint(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  var cosPhi = cos$1(phi),
+      x = cosPhi * cos$1(lambda),
+      y = cosPhi * sin$1(lambda),
+      z = sin$1(phi),
+      cx = y0 * z - z0 * y,
+      cy = z0 * x - x0 * z,
+      cz = x0 * y - y0 * x,
+      m = sqrt(cx * cx + cy * cy + cz * cz),
+      w = asin(m), // line weight = angle
+      v = m && -w / m; // area weight multiplier
+  X2 += v * cx;
+  Y2 += v * cy;
+  Z2 += v * cz;
+  W1 += w;
+  X1 += w * (x0 + (x0 = x));
+  Y1 += w * (y0 + (y0 = y));
+  Z1 += w * (z0 + (z0 = z));
+  centroidPointCartesian(x0, y0, z0);
+}
+
+function centroid(object) {
+  W0 = W1 =
+  X0 = Y0 = Z0 =
+  X1 = Y1 = Z1 =
+  X2 = Y2 = Z2 = 0;
+  geoStream(object, centroidStream);
+
+  var x = X2,
+      y = Y2,
+      z = Z2,
+      m = x * x + y * y + z * z;
+
+  // If the area-weighted ccentroid is undefined, fall back to length-weighted ccentroid.
+  if (m < epsilon2$1) {
+    x = X1, y = Y1, z = Z1;
+    // If the feature has zero length, fall back to arithmetic mean of point vectors.
+    if (W1 < epsilon$2) x = X0, y = Y0, z = Z0;
+    m = x * x + y * y + z * z;
+    // If the feature still has an undefined ccentroid, then return.
+    if (m < epsilon2$1) return [NaN, NaN];
+  }
+
+  return [atan2(y, x) * degrees$1, asin(z / sqrt(m)) * degrees$1];
+}
+
+function constant$8(x) {
+  return function() {
+    return x;
+  };
+}
+
+function compose(a, b) {
+
+  function compose(x, y) {
+    return x = a(x, y), b(x[0], x[1]);
+  }
+
+  if (a.invert && b.invert) compose.invert = function(x, y) {
+    return x = b.invert(x, y), x && a.invert(x[0], x[1]);
+  };
+
+  return compose;
+}
+
+function rotationIdentity(lambda, phi) {
+  return [abs(lambda) > pi$3 ? lambda + Math.round(-lambda / tau$3) * tau$3 : lambda, phi];
+}
+
+rotationIdentity.invert = rotationIdentity;
+
+function rotateRadians(deltaLambda, deltaPhi, deltaGamma) {
+  return (deltaLambda %= tau$3) ? (deltaPhi || deltaGamma ? compose(rotationLambda(deltaLambda), rotationPhiGamma(deltaPhi, deltaGamma))
+    : rotationLambda(deltaLambda))
+    : (deltaPhi || deltaGamma ? rotationPhiGamma(deltaPhi, deltaGamma)
+    : rotationIdentity);
+}
+
+function forwardRotationLambda(deltaLambda) {
+  return function(lambda, phi) {
+    return lambda += deltaLambda, [lambda > pi$3 ? lambda - tau$3 : lambda < -pi$3 ? lambda + tau$3 : lambda, phi];
+  };
+}
+
+function rotationLambda(deltaLambda) {
+  var rotation = forwardRotationLambda(deltaLambda);
+  rotation.invert = forwardRotationLambda(-deltaLambda);
+  return rotation;
+}
+
+function rotationPhiGamma(deltaPhi, deltaGamma) {
+  var cosDeltaPhi = cos$1(deltaPhi),
+      sinDeltaPhi = sin$1(deltaPhi),
+      cosDeltaGamma = cos$1(deltaGamma),
+      sinDeltaGamma = sin$1(deltaGamma);
+
+  function rotation(lambda, phi) {
+    var cosPhi = cos$1(phi),
+        x = cos$1(lambda) * cosPhi,
+        y = sin$1(lambda) * cosPhi,
+        z = sin$1(phi),
+        k = z * cosDeltaPhi + x * sinDeltaPhi;
+    return [
+      atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi),
+      asin(k * cosDeltaGamma + y * sinDeltaGamma)
+    ];
+  }
+
+  rotation.invert = function(lambda, phi) {
+    var cosPhi = cos$1(phi),
+        x = cos$1(lambda) * cosPhi,
+        y = sin$1(lambda) * cosPhi,
+        z = sin$1(phi),
+        k = z * cosDeltaGamma - y * sinDeltaGamma;
+    return [
+      atan2(y * cosDeltaGamma + z * sinDeltaGamma, x * cosDeltaPhi + k * sinDeltaPhi),
+      asin(k * cosDeltaPhi - x * sinDeltaPhi)
+    ];
+  };
+
+  return rotation;
+}
+
+function rotation(rotate) {
+  rotate = rotateRadians(rotate[0] * radians, rotate[1] * radians, rotate.length > 2 ? rotate[2] * radians : 0);
+
+  function forward(coordinates) {
+    coordinates = rotate(coordinates[0] * radians, coordinates[1] * radians);
+    return coordinates[0] *= degrees$1, coordinates[1] *= degrees$1, coordinates;
+  }
+
+  forward.invert = function(coordinates) {
+    coordinates = rotate.invert(coordinates[0] * radians, coordinates[1] * radians);
+    return coordinates[0] *= degrees$1, coordinates[1] *= degrees$1, coordinates;
+  };
+
+  return forward;
+}
+
+// Generates a circle centered at [0°, 0°], with a given radius and precision.
+function circleStream(stream, radius, delta, direction, t0, t1) {
+  if (!delta) return;
+  var cosRadius = cos$1(radius),
+      sinRadius = sin$1(radius),
+      step = direction * delta;
+  if (t0 == null) {
+    t0 = radius + direction * tau$3;
+    t1 = radius - step / 2;
+  } else {
+    t0 = circleRadius(cosRadius, t0);
+    t1 = circleRadius(cosRadius, t1);
+    if (direction > 0 ? t0 < t1 : t0 > t1) t0 += direction * tau$3;
+  }
+  for (var point, t = t0; direction > 0 ? t > t1 : t < t1; t -= step) {
+    point = spherical([cosRadius, -sinRadius * cos$1(t), -sinRadius * sin$1(t)]);
+    stream.point(point[0], point[1]);
+  }
+}
+
+// Returns the signed angle of a cartesian point relative to [cosRadius, 0, 0].
+function circleRadius(cosRadius, point) {
+  point = cartesian(point), point[0] -= cosRadius;
+  cartesianNormalizeInPlace(point);
+  var radius = acos(-point[1]);
+  return ((-point[2] < 0 ? -radius : radius) + tau$3 - epsilon$2) % tau$3;
+}
+
+function circle() {
+  var center = constant$8([0, 0]),
+      radius = constant$8(90),
+      precision = constant$8(6),
+      ring,
+      rotate,
+      stream = {point: point};
+
+  function point(x, y) {
+    ring.push(x = rotate(x, y));
+    x[0] *= degrees$1, x[1] *= degrees$1;
+  }
+
+  function circle() {
+    var c = center.apply(this, arguments),
+        r = radius.apply(this, arguments) * radians,
+        p = precision.apply(this, arguments) * radians;
+    ring = [];
+    rotate = rotateRadians(-c[0] * radians, -c[1] * radians, 0).invert;
+    circleStream(stream, r, p, 1);
+    c = {type: "Polygon", coordinates: [ring]};
+    ring = rotate = null;
+    return c;
+  }
+
+  circle.center = function(_) {
+    return arguments.length ? (center = typeof _ === "function" ? _ : constant$8([+_[0], +_[1]]), circle) : center;
+  };
+
+  circle.radius = function(_) {
+    return arguments.length ? (radius = typeof _ === "function" ? _ : constant$8(+_), circle) : radius;
+  };
+
+  circle.precision = function(_) {
+    return arguments.length ? (precision = typeof _ === "function" ? _ : constant$8(+_), circle) : precision;
+  };
+
+  return circle;
+}
+
+function clipBuffer() {
+  var lines = [],
+      line;
+  return {
+    point: function(x, y) {
+      line.push([x, y]);
+    },
+    lineStart: function() {
+      lines.push(line = []);
+    },
+    lineEnd: noop$2,
+    rejoin: function() {
+      if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));
+    },
+    result: function() {
+      var result = lines;
+      lines = [];
+      line = null;
+      return result;
+    }
+  };
+}
+
+function pointEqual(a, b) {
+  return abs(a[0] - b[0]) < epsilon$2 && abs(a[1] - b[1]) < epsilon$2;
+}
+
+function Intersection(point, points, other, entry) {
+  this.x = point;
+  this.z = points;
+  this.o = other; // another intersection
+  this.e = entry; // is an entry?
+  this.v = false; // visited
+  this.n = this.p = null; // next & previous
+}
+
+// A generalized polygon clipping algorithm: given a polygon that has been cut
+// into its visible line segments, and rejoins the segments by interpolating
+// along the clip edge.
+function clipRejoin(segments, compareIntersection, startInside, interpolate, stream) {
+  var subject = [],
+      clip = [],
+      i,
+      n;
+
+  segments.forEach(function(segment) {
+    if ((n = segment.length - 1) <= 0) return;
+    var n, p0 = segment[0], p1 = segment[n], x;
+
+    // If the first and last points of a segment are coincident, then treat as a
+    // closed ring. TODO if all rings are closed, then the winding order of the
+    // exterior ring should be checked.
+    if (pointEqual(p0, p1)) {
+      stream.lineStart();
+      for (i = 0; i < n; ++i) stream.point((p0 = segment[i])[0], p0[1]);
+      stream.lineEnd();
+      return;
+    }
+
+    subject.push(x = new Intersection(p0, segment, null, true));
+    clip.push(x.o = new Intersection(p0, null, x, false));
+    subject.push(x = new Intersection(p1, segment, null, false));
+    clip.push(x.o = new Intersection(p1, null, x, true));
+  });
+
+  if (!subject.length) return;
+
+  clip.sort(compareIntersection);
+  link$1(subject);
+  link$1(clip);
+
+  for (i = 0, n = clip.length; i < n; ++i) {
+    clip[i].e = startInside = !startInside;
+  }
+
+  var start = subject[0],
+      points,
+      point;
+
+  while (1) {
+    // Find first unvisited intersection.
+    var current = start,
+        isSubject = true;
+    while (current.v) if ((current = current.n) === start) return;
+    points = current.z;
+    stream.lineStart();
+    do {
+      current.v = current.o.v = true;
+      if (current.e) {
+        if (isSubject) {
+          for (i = 0, n = points.length; i < n; ++i) stream.point((point = points[i])[0], point[1]);
+        } else {
+          interpolate(current.x, current.n.x, 1, stream);
+        }
+        current = current.n;
+      } else {
+        if (isSubject) {
+          points = current.p.z;
+          for (i = points.length - 1; i >= 0; --i) stream.point((point = points[i])[0], point[1]);
+        } else {
+          interpolate(current.x, current.p.x, -1, stream);
+        }
+        current = current.p;
+      }
+      current = current.o;
+      points = current.z;
+      isSubject = !isSubject;
+    } while (!current.v);
+    stream.lineEnd();
+  }
+}
+
+function link$1(array) {
+  if (!(n = array.length)) return;
+  var n,
+      i = 0,
+      a = array[0],
+      b;
+  while (++i < n) {
+    a.n = b = array[i];
+    b.p = a;
+    a = b;
+  }
+  a.n = b = array[0];
+  b.p = a;
+}
+
+var sum$1 = adder();
+
+function polygonContains(polygon, point) {
+  var lambda = point[0],
+      phi = point[1],
+      sinPhi = sin$1(phi),
+      normal = [sin$1(lambda), -cos$1(lambda), 0],
+      angle = 0,
+      winding = 0;
+
+  sum$1.reset();
+
+  if (sinPhi === 1) phi = halfPi$2 + epsilon$2;
+  else if (sinPhi === -1) phi = -halfPi$2 - epsilon$2;
+
+  for (var i = 0, n = polygon.length; i < n; ++i) {
+    if (!(m = (ring = polygon[i]).length)) continue;
+    var ring,
+        m,
+        point0 = ring[m - 1],
+        lambda0 = point0[0],
+        phi0 = point0[1] / 2 + quarterPi,
+        sinPhi0 = sin$1(phi0),
+        cosPhi0 = cos$1(phi0);
+
+    for (var j = 0; j < m; ++j, lambda0 = lambda1, sinPhi0 = sinPhi1, cosPhi0 = cosPhi1, point0 = point1) {
+      var point1 = ring[j],
+          lambda1 = point1[0],
+          phi1 = point1[1] / 2 + quarterPi,
+          sinPhi1 = sin$1(phi1),
+          cosPhi1 = cos$1(phi1),
+          delta = lambda1 - lambda0,
+          sign$$1 = delta >= 0 ? 1 : -1,
+          absDelta = sign$$1 * delta,
+          antimeridian = absDelta > pi$3,
+          k = sinPhi0 * sinPhi1;
+
+      sum$1.add(atan2(k * sign$$1 * sin$1(absDelta), cosPhi0 * cosPhi1 + k * cos$1(absDelta)));
+      angle += antimeridian ? delta + sign$$1 * tau$3 : delta;
+
+      // Are the longitudes either side of the point’s meridian (lambda),
+      // and are the latitudes smaller than the parallel (phi)?
+      if (antimeridian ^ lambda0 >= lambda ^ lambda1 >= lambda) {
+        var arc = cartesianCross(cartesian(point0), cartesian(point1));
+        cartesianNormalizeInPlace(arc);
+        var intersection = cartesianCross(normal, arc);
+        cartesianNormalizeInPlace(intersection);
+        var phiArc = (antimeridian ^ delta >= 0 ? -1 : 1) * asin(intersection[2]);
+        if (phi > phiArc || phi === phiArc && (arc[0] || arc[1])) {
+          winding += antimeridian ^ delta >= 0 ? 1 : -1;
+        }
+      }
+    }
+  }
+
+  // First, determine whether the South pole is inside or outside:
+  //
+  // It is inside if:
+  // * the polygon winds around it in a clockwise direction.
+  // * the polygon does not (cumulatively) wind around it, but has a negative
+  //   (counter-clockwise) area.
+  //
+  // Second, count the (signed) number of times a segment crosses a lambda
+  // from the point to the South pole.  If it is zero, then the point is the
+  // same side as the South pole.
+
+  return (angle < -epsilon$2 || angle < epsilon$2 && sum$1 < -epsilon$2) ^ (winding & 1);
+}
+
+function clip(pointVisible, clipLine, interpolate, start) {
+  return function(sink) {
+    var line = clipLine(sink),
+        ringBuffer = clipBuffer(),
+        ringSink = clipLine(ringBuffer),
+        polygonStarted = false,
+        polygon,
+        segments,
+        ring;
+
+    var clip = {
+      point: point,
+      lineStart: lineStart,
+      lineEnd: lineEnd,
+      polygonStart: function() {
+        clip.point = pointRing;
+        clip.lineStart = ringStart;
+        clip.lineEnd = ringEnd;
+        segments = [];
+        polygon = [];
+      },
+      polygonEnd: function() {
+        clip.point = point;
+        clip.lineStart = lineStart;
+        clip.lineEnd = lineEnd;
+        segments = merge(segments);
+        var startInside = polygonContains(polygon, start);
+        if (segments.length) {
+          if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+          clipRejoin(segments, compareIntersection, startInside, interpolate, sink);
+        } else if (startInside) {
+          if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+          sink.lineStart();
+          interpolate(null, null, 1, sink);
+          sink.lineEnd();
+        }
+        if (polygonStarted) sink.polygonEnd(), polygonStarted = false;
+        segments = polygon = null;
+      },
+      sphere: function() {
+        sink.polygonStart();
+        sink.lineStart();
+        interpolate(null, null, 1, sink);
+        sink.lineEnd();
+        sink.polygonEnd();
+      }
+    };
+
+    function point(lambda, phi) {
+      if (pointVisible(lambda, phi)) sink.point(lambda, phi);
+    }
+
+    function pointLine(lambda, phi) {
+      line.point(lambda, phi);
+    }
+
+    function lineStart() {
+      clip.point = pointLine;
+      line.lineStart();
+    }
+
+    function lineEnd() {
+      clip.point = point;
+      line.lineEnd();
+    }
+
+    function pointRing(lambda, phi) {
+      ring.push([lambda, phi]);
+      ringSink.point(lambda, phi);
+    }
+
+    function ringStart() {
+      ringSink.lineStart();
+      ring = [];
+    }
+
+    function ringEnd() {
+      pointRing(ring[0][0], ring[0][1]);
+      ringSink.lineEnd();
+
+      var clean = ringSink.clean(),
+          ringSegments = ringBuffer.result(),
+          i, n = ringSegments.length, m,
+          segment,
+          point;
+
+      ring.pop();
+      polygon.push(ring);
+      ring = null;
+
+      if (!n) return;
+
+      // No intersections.
+      if (clean & 1) {
+        segment = ringSegments[0];
+        if ((m = segment.length - 1) > 0) {
+          if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+          sink.lineStart();
+          for (i = 0; i < m; ++i) sink.point((point = segment[i])[0], point[1]);
+          sink.lineEnd();
+        }
+        return;
+      }
+
+      // Rejoin connected segments.
+      // TODO reuse ringBuffer.rejoin()?
+      if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));
+
+      segments.push(ringSegments.filter(validSegment));
+    }
+
+    return clip;
+  };
+}
+
+function validSegment(segment) {
+  return segment.length > 1;
+}
+
+// Intersections are sorted along the clip edge. For both antimeridian cutting
+// and circle clipping, the same comparison is used.
+function compareIntersection(a, b) {
+  return ((a = a.x)[0] < 0 ? a[1] - halfPi$2 - epsilon$2 : halfPi$2 - a[1])
+       - ((b = b.x)[0] < 0 ? b[1] - halfPi$2 - epsilon$2 : halfPi$2 - b[1]);
+}
+
+var clipAntimeridian = clip(
+  function() { return true; },
+  clipAntimeridianLine,
+  clipAntimeridianInterpolate,
+  [-pi$3, -halfPi$2]
+);
+
+// Takes a line and cuts into visible segments. Return values: 0 - there were
+// intersections or the line was empty; 1 - no intersections; 2 - there were
+// intersections, and the first and last segments should be rejoined.
+function clipAntimeridianLine(stream) {
+  var lambda0 = NaN,
+      phi0 = NaN,
+      sign0 = NaN,
+      clean; // no intersections
+
+  return {
+    lineStart: function() {
+      stream.lineStart();
+      clean = 1;
+    },
+    point: function(lambda1, phi1) {
+      var sign1 = lambda1 > 0 ? pi$3 : -pi$3,
+          delta = abs(lambda1 - lambda0);
+      if (abs(delta - pi$3) < epsilon$2) { // line crosses a pole
+        stream.point(lambda0, phi0 = (phi0 + phi1) / 2 > 0 ? halfPi$2 : -halfPi$2);
+        stream.point(sign0, phi0);
+        stream.lineEnd();
+        stream.lineStart();
+        stream.point(sign1, phi0);
+        stream.point(lambda1, phi0);
+        clean = 0;
+      } else if (sign0 !== sign1 && delta >= pi$3) { // line crosses antimeridian
+        if (abs(lambda0 - sign0) < epsilon$2) lambda0 -= sign0 * epsilon$2; // handle degeneracies
+        if (abs(lambda1 - sign1) < epsilon$2) lambda1 -= sign1 * epsilon$2;
+        phi0 = clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1);
+        stream.point(sign0, phi0);
+        stream.lineEnd();
+        stream.lineStart();
+        stream.point(sign1, phi0);
+        clean = 0;
+      }
+      stream.point(lambda0 = lambda1, phi0 = phi1);
+      sign0 = sign1;
+    },
+    lineEnd: function() {
+      stream.lineEnd();
+      lambda0 = phi0 = NaN;
+    },
+    clean: function() {
+      return 2 - clean; // if intersections, rejoin first and last segments
+    }
+  };
+}
+
+function clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1) {
+  var cosPhi0,
+      cosPhi1,
+      sinLambda0Lambda1 = sin$1(lambda0 - lambda1);
+  return abs(sinLambda0Lambda1) > epsilon$2
+      ? atan((sin$1(phi0) * (cosPhi1 = cos$1(phi1)) * sin$1(lambda1)
+          - sin$1(phi1) * (cosPhi0 = cos$1(phi0)) * sin$1(lambda0))
+          / (cosPhi0 * cosPhi1 * sinLambda0Lambda1))
+      : (phi0 + phi1) / 2;
+}
+
+function clipAntimeridianInterpolate(from, to, direction, stream) {
+  var phi;
+  if (from == null) {
+    phi = direction * halfPi$2;
+    stream.point(-pi$3, phi);
+    stream.point(0, phi);
+    stream.point(pi$3, phi);
+    stream.point(pi$3, 0);
+    stream.point(pi$3, -phi);
+    stream.point(0, -phi);
+    stream.point(-pi$3, -phi);
+    stream.point(-pi$3, 0);
+    stream.point(-pi$3, phi);
+  } else if (abs(from[0] - to[0]) > epsilon$2) {
+    var lambda = from[0] < to[0] ? pi$3 : -pi$3;
+    phi = direction * lambda / 2;
+    stream.point(-lambda, phi);
+    stream.point(0, phi);
+    stream.point(lambda, phi);
+  } else {
+    stream.point(to[0], to[1]);
+  }
+}
+
+function clipCircle(radius) {
+  var cr = cos$1(radius),
+      delta = 6 * radians,
+      smallRadius = cr > 0,
+      notHemisphere = abs(cr) > epsilon$2; // TODO optimise for this common case
+
+  function interpolate(from, to, direction, stream) {
+    circleStream(stream, radius, delta, direction, from, to);
+  }
+
+  function visible(lambda, phi) {
+    return cos$1(lambda) * cos$1(phi) > cr;
+  }
+
+  // Takes a line and cuts into visible segments. Return values used for polygon
+  // clipping: 0 - there were intersections or the line was empty; 1 - no
+  // intersections 2 - there were intersections, and the first and last segments
+  // should be rejoined.
+  function clipLine(stream) {
+    var point0, // previous point
+        c0, // code for previous point
+        v0, // visibility of previous point
+        v00, // visibility of first point
+        clean; // no intersections
+    return {
+      lineStart: function() {
+        v00 = v0 = false;
+        clean = 1;
+      },
+      point: function(lambda, phi) {
+        var point1 = [lambda, phi],
+            point2,
+            v = visible(lambda, phi),
+            c = smallRadius
+              ? v ? 0 : code(lambda, phi)
+              : v ? code(lambda + (lambda < 0 ? pi$3 : -pi$3), phi) : 0;
+        if (!point0 && (v00 = v0 = v)) stream.lineStart();
+        // Handle degeneracies.
+        // TODO ignore if not clipping polygons.
+        if (v !== v0) {
+          point2 = intersect(point0, point1);
+          if (!point2 || pointEqual(point0, point2) || pointEqual(point1, point2)) {
+            point1[0] += epsilon$2;
+            point1[1] += epsilon$2;
+            v = visible(point1[0], point1[1]);
+          }
+        }
+        if (v !== v0) {
+          clean = 0;
+          if (v) {
+            // outside going in
+            stream.lineStart();
+            point2 = intersect(point1, point0);
+            stream.point(point2[0], point2[1]);
+          } else {
+            // inside going out
+            point2 = intersect(point0, point1);
+            stream.point(point2[0], point2[1]);
+            stream.lineEnd();
+          }
+          point0 = point2;
+        } else if (notHemisphere && point0 && smallRadius ^ v) {
+          var t;
+          // If the codes for two points are different, or are both zero,
+          // and there this segment intersects with the small circle.
+          if (!(c & c0) && (t = intersect(point1, point0, true))) {
+            clean = 0;
+            if (smallRadius) {
+              stream.lineStart();
+              stream.point(t[0][0], t[0][1]);
+              stream.point(t[1][0], t[1][1]);
+              stream.lineEnd();
+            } else {
+              stream.point(t[1][0], t[1][1]);
+              stream.lineEnd();
+              stream.lineStart();
+              stream.point(t[0][0], t[0][1]);
+            }
+          }
+        }
+        if (v && (!point0 || !pointEqual(point0, point1))) {
+          stream.point(point1[0], point1[1]);
+        }
+        point0 = point1, v0 = v, c0 = c;
+      },
+      lineEnd: function() {
+        if (v0) stream.lineEnd();
+        point0 = null;
+      },
+      // Rejoin first and last segments if there were intersections and the first
+      // and last points were visible.
+      clean: function() {
+        return clean | ((v00 && v0) << 1);
+      }
+    };
+  }
+
+  // Intersects the great circle between a and b with the clip circle.
+  function intersect(a, b, two) {
+    var pa = cartesian(a),
+        pb = cartesian(b);
+
+    // We have two planes, n1.p = d1 and n2.p = d2.
+    // Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 ⨯ n2).
+    var n1 = [1, 0, 0], // normal
+        n2 = cartesianCross(pa, pb),
+        n2n2 = cartesianDot(n2, n2),
+        n1n2 = n2[0], // cartesianDot(n1, n2),
+        determinant = n2n2 - n1n2 * n1n2;
+
+    // Two polar points.
+    if (!determinant) return !two && a;
+
+    var c1 =  cr * n2n2 / determinant,
+        c2 = -cr * n1n2 / determinant,
+        n1xn2 = cartesianCross(n1, n2),
+        A = cartesianScale(n1, c1),
+        B = cartesianScale(n2, c2);
+    cartesianAddInPlace(A, B);
+
+    // Solve |p(t)|^2 = 1.
+    var u = n1xn2,
+        w = cartesianDot(A, u),
+        uu = cartesianDot(u, u),
+        t2 = w * w - uu * (cartesianDot(A, A) - 1);
+
+    if (t2 < 0) return;
+
+    var t = sqrt(t2),
+        q = cartesianScale(u, (-w - t) / uu);
+    cartesianAddInPlace(q, A);
+    q = spherical(q);
+
+    if (!two) return q;
+
+    // Two intersection points.
+    var lambda0 = a[0],
+        lambda1 = b[0],
+        phi0 = a[1],
+        phi1 = b[1],
+        z;
+
+    if (lambda1 < lambda0) z = lambda0, lambda0 = lambda1, lambda1 = z;
+
+    var delta = lambda1 - lambda0,
+        polar = abs(delta - pi$3) < epsilon$2,
+        meridian = polar || delta < epsilon$2;
+
+    if (!polar && phi1 < phi0) z = phi0, phi0 = phi1, phi1 = z;
+
+    // Check that the first point is between a and b.
+    if (meridian
+        ? polar
+          ? phi0 + phi1 > 0 ^ q[1] < (abs(q[0] - lambda0) < epsilon$2 ? phi0 : phi1)
+          : phi0 <= q[1] && q[1] <= phi1
+        : delta > pi$3 ^ (lambda0 <= q[0] && q[0] <= lambda1)) {
+      var q1 = cartesianScale(u, (-w + t) / uu);
+      cartesianAddInPlace(q1, A);
+      return [q, spherical(q1)];
+    }
+  }
+
+  // Generates a 4-bit vector representing the location of a point relative to
+  // the small circle's bounding box.
+  function code(lambda, phi) {
+    var r = smallRadius ? radius : pi$3 - radius,
+        code = 0;
+    if (lambda < -r) code |= 1; // left
+    else if (lambda > r) code |= 2; // right
+    if (phi < -r) code |= 4; // below
+    else if (phi > r) code |= 8; // above
+    return code;
+  }
+
+  return clip(visible, clipLine, interpolate, smallRadius ? [0, -radius] : [-pi$3, radius - pi$3]);
+}
+
+function clipLine(a, b, x0, y0, x1, y1) {
+  var ax = a[0],
+      ay = a[1],
+      bx = b[0],
+      by = b[1],
+      t0 = 0,
+      t1 = 1,
+      dx = bx - ax,
+      dy = by - ay,
+      r;
+
+  r = x0 - ax;
+  if (!dx && r > 0) return;
+  r /= dx;
+  if (dx < 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  } else if (dx > 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  }
+
+  r = x1 - ax;
+  if (!dx && r < 0) return;
+  r /= dx;
+  if (dx < 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  } else if (dx > 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  }
+
+  r = y0 - ay;
+  if (!dy && r > 0) return;
+  r /= dy;
+  if (dy < 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  } else if (dy > 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  }
+
+  r = y1 - ay;
+  if (!dy && r < 0) return;
+  r /= dy;
+  if (dy < 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  } else if (dy > 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  }
+
+  if (t0 > 0) a[0] = ax + t0 * dx, a[1] = ay + t0 * dy;
+  if (t1 < 1) b[0] = ax + t1 * dx, b[1] = ay + t1 * dy;
+  return true;
+}
+
+var clipMax = 1e9, clipMin = -clipMax;
+
+// TODO Use d3-polygon’s polygonContains here for the ring check?
+// TODO Eliminate duplicate buffering in clipBuffer and polygon.push?
+
+function clipRectangle(x0, y0, x1, y1) {
+
+  function visible(x, y) {
+    return x0 <= x && x <= x1 && y0 <= y && y <= y1;
+  }
+
+  function interpolate(from, to, direction, stream) {
+    var a = 0, a1 = 0;
+    if (from == null
+        || (a = corner(from, direction)) !== (a1 = corner(to, direction))
+        || comparePoint(from, to) < 0 ^ direction > 0) {
+      do stream.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);
+      while ((a = (a + direction + 4) % 4) !== a1);
+    } else {
+      stream.point(to[0], to[1]);
+    }
+  }
+
+  function corner(p, direction) {
+    return abs(p[0] - x0) < epsilon$2 ? direction > 0 ? 0 : 3
+        : abs(p[0] - x1) < epsilon$2 ? direction > 0 ? 2 : 1
+        : abs(p[1] - y0) < epsilon$2 ? direction > 0 ? 1 : 0
+        : direction > 0 ? 3 : 2; // abs(p[1] - y1) < epsilon
+  }
+
+  function compareIntersection(a, b) {
+    return comparePoint(a.x, b.x);
+  }
+
+  function comparePoint(a, b) {
+    var ca = corner(a, 1),
+        cb = corner(b, 1);
+    return ca !== cb ? ca - cb
+        : ca === 0 ? b[1] - a[1]
+        : ca === 1 ? a[0] - b[0]
+        : ca === 2 ? a[1] - b[1]
+        : b[0] - a[0];
+  }
+
+  return function(stream) {
+    var activeStream = stream,
+        bufferStream = clipBuffer(),
+        segments,
+        polygon,
+        ring,
+        x__, y__, v__, // first point
+        x_, y_, v_, // previous point
+        first,
+        clean;
+
+    var clipStream = {
+      point: point,
+      lineStart: lineStart,
+      lineEnd: lineEnd,
+      polygonStart: polygonStart,
+      polygonEnd: polygonEnd
+    };
+
+    function point(x, y) {
+      if (visible(x, y)) activeStream.point(x, y);
+    }
+
+    function polygonInside() {
+      var winding = 0;
+
+      for (var i = 0, n = polygon.length; i < n; ++i) {
+        for (var ring = polygon[i], j = 1, m = ring.length, point = ring[0], a0, a1, b0 = point[0], b1 = point[1]; j < m; ++j) {
+          a0 = b0, a1 = b1, point = ring[j], b0 = point[0], b1 = point[1];
+          if (a1 <= y1) { if (b1 > y1 && (b0 - a0) * (y1 - a1) > (b1 - a1) * (x0 - a0)) ++winding; }
+          else { if (b1 <= y1 && (b0 - a0) * (y1 - a1) < (b1 - a1) * (x0 - a0)) --winding; }
+        }
+      }
+
+      return winding;
+    }
+
+    // Buffer geometry within a polygon and then clip it en masse.
+    function polygonStart() {
+      activeStream = bufferStream, segments = [], polygon = [], clean = true;
+    }
+
+    function polygonEnd() {
+      var startInside = polygonInside(),
+          cleanInside = clean && startInside,
+          visible = (segments = merge(segments)).length;
+      if (cleanInside || visible) {
+        stream.polygonStart();
+        if (cleanInside) {
+          stream.lineStart();
+          interpolate(null, null, 1, stream);
+          stream.lineEnd();
+        }
+        if (visible) {
+          clipRejoin(segments, compareIntersection, startInside, interpolate, stream);
+        }
+        stream.polygonEnd();
+      }
+      activeStream = stream, segments = polygon = ring = null;
+    }
+
+    function lineStart() {
+      clipStream.point = linePoint;
+      if (polygon) polygon.push(ring = []);
+      first = true;
+      v_ = false;
+      x_ = y_ = NaN;
+    }
+
+    // TODO rather than special-case polygons, simply handle them separately.
+    // Ideally, coincident intersection points should be jittered to avoid
+    // clipping issues.
+    function lineEnd() {
+      if (segments) {
+        linePoint(x__, y__);
+        if (v__ && v_) bufferStream.rejoin();
+        segments.push(bufferStream.result());
+      }
+      clipStream.point = point;
+      if (v_) activeStream.lineEnd();
+    }
+
+    function linePoint(x, y) {
+      var v = visible(x, y);
+      if (polygon) ring.push([x, y]);
+      if (first) {
+        x__ = x, y__ = y, v__ = v;
+        first = false;
+        if (v) {
+          activeStream.lineStart();
+          activeStream.point(x, y);
+        }
+      } else {
+        if (v && v_) activeStream.point(x, y);
+        else {
+          var a = [x_ = Math.max(clipMin, Math.min(clipMax, x_)), y_ = Math.max(clipMin, Math.min(clipMax, y_))],
+              b = [x = Math.max(clipMin, Math.min(clipMax, x)), y = Math.max(clipMin, Math.min(clipMax, y))];
+          if (clipLine(a, b, x0, y0, x1, y1)) {
+            if (!v_) {
+              activeStream.lineStart();
+              activeStream.point(a[0], a[1]);
+            }
+            activeStream.point(b[0], b[1]);
+            if (!v) activeStream.lineEnd();
+            clean = false;
+          } else if (v) {
+            activeStream.lineStart();
+            activeStream.point(x, y);
+            clean = false;
+          }
+        }
+      }
+      x_ = x, y_ = y, v_ = v;
+    }
+
+    return clipStream;
+  };
+}
+
+function extent$1() {
+  var x0 = 0,
+      y0 = 0,
+      x1 = 960,
+      y1 = 500,
+      cache,
+      cacheStream,
+      clip;
+
+  return clip = {
+    stream: function(stream) {
+      return cache && cacheStream === stream ? cache : cache = clipRectangle(x0, y0, x1, y1)(cacheStream = stream);
+    },
+    extent: function(_) {
+      return arguments.length ? (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1], cache = cacheStream = null, clip) : [[x0, y0], [x1, y1]];
+    }
+  };
+}
+
+var lengthSum = adder(),
+    lambda0$2,
+    sinPhi0$1,
+    cosPhi0$1;
+
+var lengthStream = {
+  sphere: noop$2,
+  point: noop$2,
+  lineStart: lengthLineStart,
+  lineEnd: noop$2,
+  polygonStart: noop$2,
+  polygonEnd: noop$2
+};
+
+function lengthLineStart() {
+  lengthStream.point = lengthPointFirst;
+  lengthStream.lineEnd = lengthLineEnd;
+}
+
+function lengthLineEnd() {
+  lengthStream.point = lengthStream.lineEnd = noop$2;
+}
+
+function lengthPointFirst(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  lambda0$2 = lambda, sinPhi0$1 = sin$1(phi), cosPhi0$1 = cos$1(phi);
+  lengthStream.point = lengthPoint;
+}
+
+function lengthPoint(lambda, phi) {
+  lambda *= radians, phi *= radians;
+  var sinPhi = sin$1(phi),
+      cosPhi = cos$1(phi),
+      delta = abs(lambda - lambda0$2),
+      cosDelta = cos$1(delta),
+      sinDelta = sin$1(delta),
+      x = cosPhi * sinDelta,
+      y = cosPhi0$1 * sinPhi - sinPhi0$1 * cosPhi * cosDelta,
+      z = sinPhi0$1 * sinPhi + cosPhi0$1 * cosPhi * cosDelta;
+  lengthSum.add(atan2(sqrt(x * x + y * y), z));
+  lambda0$2 = lambda, sinPhi0$1 = sinPhi, cosPhi0$1 = cosPhi;
+}
+
+function length$1(object) {
+  lengthSum.reset();
+  geoStream(object, lengthStream);
+  return +lengthSum;
+}
+
+var coordinates = [null, null],
+    object$1 = {type: "LineString", coordinates: coordinates};
+
+function distance(a, b) {
+  coordinates[0] = a;
+  coordinates[1] = b;
+  return length$1(object$1);
+}
+
+var containsObjectType = {
+  Feature: function(object, point) {
+    return containsGeometry(object.geometry, point);
+  },
+  FeatureCollection: function(object, point) {
+    var features = object.features, i = -1, n = features.length;
+    while (++i < n) if (containsGeometry(features[i].geometry, point)) return true;
+    return false;
+  }
+};
+
+var containsGeometryType = {
+  Sphere: function() {
+    return true;
+  },
+  Point: function(object, point) {
+    return containsPoint(object.coordinates, point);
+  },
+  MultiPoint: function(object, point) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) if (containsPoint(coordinates[i], point)) return true;
+    return false;
+  },
+  LineString: function(object, point) {
+    return containsLine(object.coordinates, point);
+  },
+  MultiLineString: function(object, point) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) if (containsLine(coordinates[i], point)) return true;
+    return false;
+  },
+  Polygon: function(object, point) {
+    return containsPolygon(object.coordinates, point);
+  },
+  MultiPolygon: function(object, point) {
+    var coordinates = object.coordinates, i = -1, n = coordinates.length;
+    while (++i < n) if (containsPolygon(coordinates[i], point)) return true;
+    return false;
+  },
+  GeometryCollection: function(object, point) {
+    var geometries = object.geometries, i = -1, n = geometries.length;
+    while (++i < n) if (containsGeometry(geometries[i], point)) return true;
+    return false;
+  }
+};
+
+function containsGeometry(geometry, point) {
+  return geometry && containsGeometryType.hasOwnProperty(geometry.type)
+      ? containsGeometryType[geometry.type](geometry, point)
+      : false;
+}
+
+function containsPoint(coordinates, point) {
+  return distance(coordinates, point) === 0;
+}
+
+function containsLine(coordinates, point) {
+  var ab = distance(coordinates[0], coordinates[1]),
+      ao = distance(coordinates[0], point),
+      ob = distance(point, coordinates[1]);
+  return ao + ob <= ab + epsilon$2;
+}
+
+function containsPolygon(coordinates, point) {
+  return !!polygonContains(coordinates.map(ringRadians), pointRadians(point));
+}
+
+function ringRadians(ring) {
+  return ring = ring.map(pointRadians), ring.pop(), ring;
+}
+
+function pointRadians(point) {
+  return [point[0] * radians, point[1] * radians];
+}
+
+function contains$1(object, point) {
+  return (object && containsObjectType.hasOwnProperty(object.type)
+      ? containsObjectType[object.type]
+      : containsGeometry)(object, point);
+}
+
+function graticuleX(y0, y1, dy) {
+  var y = sequence(y0, y1 - epsilon$2, dy).concat(y1);
+  return function(x) { return y.map(function(y) { return [x, y]; }); };
+}
+
+function graticuleY(x0, x1, dx) {
+  var x = sequence(x0, x1 - epsilon$2, dx).concat(x1);
+  return function(y) { return x.map(function(x) { return [x, y]; }); };
+}
+
+function graticule() {
+  var x1, x0, X1, X0,
+      y1, y0, Y1, Y0,
+      dx = 10, dy = dx, DX = 90, DY = 360,
+      x, y, X, Y,
+      precision = 2.5;
+
+  function graticule() {
+    return {type: "MultiLineString", coordinates: lines()};
+  }
+
+  function lines() {
+    return sequence(ceil(X0 / DX) * DX, X1, DX).map(X)
+        .concat(sequence(ceil(Y0 / DY) * DY, Y1, DY).map(Y))
+        .concat(sequence(ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return abs(x % DX) > epsilon$2; }).map(x))
+        .concat(sequence(ceil(y0 / dy) * dy, y1, dy).filter(function(y) { return abs(y % DY) > epsilon$2; }).map(y));
+  }
+
+  graticule.lines = function() {
+    return lines().map(function(coordinates) { return {type: "LineString", coordinates: coordinates}; });
+  };
+
+  graticule.outline = function() {
+    return {
+      type: "Polygon",
+      coordinates: [
+        X(X0).concat(
+        Y(Y1).slice(1),
+        X(X1).reverse().slice(1),
+        Y(Y0).reverse().slice(1))
+      ]
+    };
+  };
+
+  graticule.extent = function(_) {
+    if (!arguments.length) return graticule.extentMinor();
+    return graticule.extentMajor(_).extentMinor(_);
+  };
+
+  graticule.extentMajor = function(_) {
+    if (!arguments.length) return [[X0, Y0], [X1, Y1]];
+    X0 = +_[0][0], X1 = +_[1][0];
+    Y0 = +_[0][1], Y1 = +_[1][1];
+    if (X0 > X1) _ = X0, X0 = X1, X1 = _;
+    if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;
+    return graticule.precision(precision);
+  };
+
+  graticule.extentMinor = function(_) {
+    if (!arguments.length) return [[x0, y0], [x1, y1]];
+    x0 = +_[0][0], x1 = +_[1][0];
+    y0 = +_[0][1], y1 = +_[1][1];
+    if (x0 > x1) _ = x0, x0 = x1, x1 = _;
+    if (y0 > y1) _ = y0, y0 = y1, y1 = _;
+    return graticule.precision(precision);
+  };
+
+  graticule.step = function(_) {
+    if (!arguments.length) return graticule.stepMinor();
+    return graticule.stepMajor(_).stepMinor(_);
+  };
+
+  graticule.stepMajor = function(_) {
+    if (!arguments.length) return [DX, DY];
+    DX = +_[0], DY = +_[1];
+    return graticule;
+  };
+
+  graticule.stepMinor = function(_) {
+    if (!arguments.length) return [dx, dy];
+    dx = +_[0], dy = +_[1];
+    return graticule;
+  };
+
+  graticule.precision = function(_) {
+    if (!arguments.length) return precision;
+    precision = +_;
+    x = graticuleX(y0, y1, 90);
+    y = graticuleY(x0, x1, precision);
+    X = graticuleX(Y0, Y1, 90);
+    Y = graticuleY(X0, X1, precision);
+    return graticule;
+  };
+
+  return graticule
+      .extentMajor([[-180, -90 + epsilon$2], [180, 90 - epsilon$2]])
+      .extentMinor([[-180, -80 - epsilon$2], [180, 80 + epsilon$2]]);
+}
+
+function graticule10() {
+  return graticule()();
+}
+
+function interpolate$1(a, b) {
+  var x0 = a[0] * radians,
+      y0 = a[1] * radians,
+      x1 = b[0] * radians,
+      y1 = b[1] * radians,
+      cy0 = cos$1(y0),
+      sy0 = sin$1(y0),
+      cy1 = cos$1(y1),
+      sy1 = sin$1(y1),
+      kx0 = cy0 * cos$1(x0),
+      ky0 = cy0 * sin$1(x0),
+      kx1 = cy1 * cos$1(x1),
+      ky1 = cy1 * sin$1(x1),
+      d = 2 * asin(sqrt(haversin(y1 - y0) + cy0 * cy1 * haversin(x1 - x0))),
+      k = sin$1(d);
+
+  var interpolate = d ? function(t) {
+    var B = sin$1(t *= d) / k,
+        A = sin$1(d - t) / k,
+        x = A * kx0 + B * kx1,
+        y = A * ky0 + B * ky1,
+        z = A * sy0 + B * sy1;
+    return [
+      atan2(y, x) * degrees$1,
+      atan2(z, sqrt(x * x + y * y)) * degrees$1
+    ];
+  } : function() {
+    return [x0 * degrees$1, y0 * degrees$1];
+  };
+
+  interpolate.distance = d;
+
+  return interpolate;
+}
+
+function identity$4(x) {
+  return x;
+}
+
+var areaSum$1 = adder(),
+    areaRingSum$1 = adder(),
+    x00,
+    y00,
+    x0$1,
+    y0$1;
+
+var areaStream$1 = {
+  point: noop$2,
+  lineStart: noop$2,
+  lineEnd: noop$2,
+  polygonStart: function() {
+    areaStream$1.lineStart = areaRingStart$1;
+    areaStream$1.lineEnd = areaRingEnd$1;
+  },
+  polygonEnd: function() {
+    areaStream$1.lineStart = areaStream$1.lineEnd = areaStream$1.point = noop$2;
+    areaSum$1.add(abs(areaRingSum$1));
+    areaRingSum$1.reset();
+  },
+  result: function() {
+    var area = areaSum$1 / 2;
+    areaSum$1.reset();
+    return area;
+  }
+};
+
+function areaRingStart$1() {
+  areaStream$1.point = areaPointFirst$1;
+}
+
+function areaPointFirst$1(x, y) {
+  areaStream$1.point = areaPoint$1;
+  x00 = x0$1 = x, y00 = y0$1 = y;
+}
+
+function areaPoint$1(x, y) {
+  areaRingSum$1.add(y0$1 * x - x0$1 * y);
+  x0$1 = x, y0$1 = y;
+}
+
+function areaRingEnd$1() {
+  areaPoint$1(x00, y00);
+}
+
+var x0$2 = Infinity,
+    y0$2 = x0$2,
+    x1 = -x0$2,
+    y1 = x1;
+
+var boundsStream$1 = {
+  point: boundsPoint$1,
+  lineStart: noop$2,
+  lineEnd: noop$2,
+  polygonStart: noop$2,
+  polygonEnd: noop$2,
+  result: function() {
+    var bounds = [[x0$2, y0$2], [x1, y1]];
+    x1 = y1 = -(y0$2 = x0$2 = Infinity);
+    return bounds;
+  }
+};
+
+function boundsPoint$1(x, y) {
+  if (x < x0$2) x0$2 = x;
+  if (x > x1) x1 = x;
+  if (y < y0$2) y0$2 = y;
+  if (y > y1) y1 = y;
+}
+
+// TODO Enforce positive area for exterior, negative area for interior?
+
+var X0$1 = 0,
+    Y0$1 = 0,
+    Z0$1 = 0,
+    X1$1 = 0,
+    Y1$1 = 0,
+    Z1$1 = 0,
+    X2$1 = 0,
+    Y2$1 = 0,
+    Z2$1 = 0,
+    x00$1,
+    y00$1,
+    x0$3,
+    y0$3;
+
+var centroidStream$1 = {
+  point: centroidPoint$1,
+  lineStart: centroidLineStart$1,
+  lineEnd: centroidLineEnd$1,
+  polygonStart: function() {
+    centroidStream$1.lineStart = centroidRingStart$1;
+    centroidStream$1.lineEnd = centroidRingEnd$1;
+  },
+  polygonEnd: function() {
+    centroidStream$1.point = centroidPoint$1;
+    centroidStream$1.lineStart = centroidLineStart$1;
+    centroidStream$1.lineEnd = centroidLineEnd$1;
+  },
+  result: function() {
+    var centroid = Z2$1 ? [X2$1 / Z2$1, Y2$1 / Z2$1]
+        : Z1$1 ? [X1$1 / Z1$1, Y1$1 / Z1$1]
+        : Z0$1 ? [X0$1 / Z0$1, Y0$1 / Z0$1]
+        : [NaN, NaN];
+    X0$1 = Y0$1 = Z0$1 =
+    X1$1 = Y1$1 = Z1$1 =
+    X2$1 = Y2$1 = Z2$1 = 0;
+    return centroid;
+  }
+};
+
+function centroidPoint$1(x, y) {
+  X0$1 += x;
+  Y0$1 += y;
+  ++Z0$1;
+}
+
+function centroidLineStart$1() {
+  centroidStream$1.point = centroidPointFirstLine;
+}
+
+function centroidPointFirstLine(x, y) {
+  centroidStream$1.point = centroidPointLine;
+  centroidPoint$1(x0$3 = x, y0$3 = y);
+}
+
+function centroidPointLine(x, y) {
+  var dx = x - x0$3, dy = y - y0$3, z = sqrt(dx * dx + dy * dy);
+  X1$1 += z * (x0$3 + x) / 2;
+  Y1$1 += z * (y0$3 + y) / 2;
+  Z1$1 += z;
+  centroidPoint$1(x0$3 = x, y0$3 = y);
+}
+
+function centroidLineEnd$1() {
+  centroidStream$1.point = centroidPoint$1;
+}
+
+function centroidRingStart$1() {
+  centroidStream$1.point = centroidPointFirstRing;
+}
+
+function centroidRingEnd$1() {
+  centroidPointRing(x00$1, y00$1);
+}
+
+function centroidPointFirstRing(x, y) {
+  centroidStream$1.point = centroidPointRing;
+  centroidPoint$1(x00$1 = x0$3 = x, y00$1 = y0$3 = y);
+}
+
+function centroidPointRing(x, y) {
+  var dx = x - x0$3,
+      dy = y - y0$3,
+      z = sqrt(dx * dx + dy * dy);
+
+  X1$1 += z * (x0$3 + x) / 2;
+  Y1$1 += z * (y0$3 + y) / 2;
+  Z1$1 += z;
+
+  z = y0$3 * x - x0$3 * y;
+  X2$1 += z * (x0$3 + x);
+  Y2$1 += z * (y0$3 + y);
+  Z2$1 += z * 3;
+  centroidPoint$1(x0$3 = x, y0$3 = y);
+}
+
+function PathContext(context) {
+  this._context = context;
+}
+
+PathContext.prototype = {
+  _radius: 4.5,
+  pointRadius: function(_) {
+    return this._radius = _, this;
+  },
+  polygonStart: function() {
+    this._line = 0;
+  },
+  polygonEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line === 0) this._context.closePath();
+    this._point = NaN;
+  },
+  point: function(x, y) {
+    switch (this._point) {
+      case 0: {
+        this._context.moveTo(x, y);
+        this._point = 1;
+        break;
+      }
+      case 1: {
+        this._context.lineTo(x, y);
+        break;
+      }
+      default: {
+        this._context.moveTo(x + this._radius, y);
+        this._context.arc(x, y, this._radius, 0, tau$3);
+        break;
+      }
+    }
+  },
+  result: noop$2
+};
+
+var lengthSum$1 = adder(),
+    lengthRing,
+    x00$2,
+    y00$2,
+    x0$4,
+    y0$4;
+
+var lengthStream$1 = {
+  point: noop$2,
+  lineStart: function() {
+    lengthStream$1.point = lengthPointFirst$1;
+  },
+  lineEnd: function() {
+    if (lengthRing) lengthPoint$1(x00$2, y00$2);
+    lengthStream$1.point = noop$2;
+  },
+  polygonStart: function() {
+    lengthRing = true;
+  },
+  polygonEnd: function() {
+    lengthRing = null;
+  },
+  result: function() {
+    var length = +lengthSum$1;
+    lengthSum$1.reset();
+    return length;
+  }
+};
+
+function lengthPointFirst$1(x, y) {
+  lengthStream$1.point = lengthPoint$1;
+  x00$2 = x0$4 = x, y00$2 = y0$4 = y;
+}
+
+function lengthPoint$1(x, y) {
+  x0$4 -= x, y0$4 -= y;
+  lengthSum$1.add(sqrt(x0$4 * x0$4 + y0$4 * y0$4));
+  x0$4 = x, y0$4 = y;
+}
+
+function PathString() {
+  this._string = [];
+}
+
+PathString.prototype = {
+  _radius: 4.5,
+  _circle: circle$1(4.5),
+  pointRadius: function(_) {
+    if ((_ = +_) !== this._radius) this._radius = _, this._circle = null;
+    return this;
+  },
+  polygonStart: function() {
+    this._line = 0;
+  },
+  polygonEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line === 0) this._string.push("Z");
+    this._point = NaN;
+  },
+  point: function(x, y) {
+    switch (this._point) {
+      case 0: {
+        this._string.push("M", x, ",", y);
+        this._point = 1;
+        break;
+      }
+      case 1: {
+        this._string.push("L", x, ",", y);
+        break;
+      }
+      default: {
+        if (this._circle == null) this._circle = circle$1(this._radius);
+        this._string.push("M", x, ",", y, this._circle);
+        break;
+      }
+    }
+  },
+  result: function() {
+    if (this._string.length) {
+      var result = this._string.join("");
+      this._string = [];
+      return result;
+    } else {
+      return null;
+    }
+  }
+};
+
+function circle$1(radius) {
+  return "m0," + radius
+      + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius
+      + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius
+      + "z";
+}
+
+function index$1(projection, context) {
+  var pointRadius = 4.5,
+      projectionStream,
+      contextStream;
+
+  function path(object) {
+    if (object) {
+      if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments));
+      geoStream(object, projectionStream(contextStream));
+    }
+    return contextStream.result();
+  }
+
+  path.area = function(object) {
+    geoStream(object, projectionStream(areaStream$1));
+    return areaStream$1.result();
+  };
+
+  path.measure = function(object) {
+    geoStream(object, projectionStream(lengthStream$1));
+    return lengthStream$1.result();
+  };
+
+  path.bounds = function(object) {
+    geoStream(object, projectionStream(boundsStream$1));
+    return boundsStream$1.result();
+  };
+
+  path.centroid = function(object) {
+    geoStream(object, projectionStream(centroidStream$1));
+    return centroidStream$1.result();
+  };
+
+  path.projection = function(_) {
+    return arguments.length ? (projectionStream = _ == null ? (projection = null, identity$4) : (projection = _).stream, path) : projection;
+  };
+
+  path.context = function(_) {
+    if (!arguments.length) return context;
+    contextStream = _ == null ? (context = null, new PathString) : new PathContext(context = _);
+    if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
+    return path;
+  };
+
+  path.pointRadius = function(_) {
+    if (!arguments.length) return pointRadius;
+    pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_);
+    return path;
+  };
+
+  return path.projection(projection).context(context);
+}
+
+function transform(methods) {
+  return {
+    stream: transformer(methods)
+  };
+}
+
+function transformer(methods) {
+  return function(stream) {
+    var s = new TransformStream;
+    for (var key in methods) s[key] = methods[key];
+    s.stream = stream;
+    return s;
+  };
+}
+
+function TransformStream() {}
+
+TransformStream.prototype = {
+  constructor: TransformStream,
+  point: function(x, y) { this.stream.point(x, y); },
+  sphere: function() { this.stream.sphere(); },
+  lineStart: function() { this.stream.lineStart(); },
+  lineEnd: function() { this.stream.lineEnd(); },
+  polygonStart: function() { this.stream.polygonStart(); },
+  polygonEnd: function() { this.stream.polygonEnd(); }
+};
+
+function fit(projection, fitBounds, object) {
+  var clip = projection.clipExtent && projection.clipExtent();
+  projection.scale(150).translate([0, 0]);
+  if (clip != null) projection.clipExtent(null);
+  geoStream(object, projection.stream(boundsStream$1));
+  fitBounds(boundsStream$1.result());
+  if (clip != null) projection.clipExtent(clip);
+  return projection;
+}
+
+function fitExtent(projection, extent, object) {
+  return fit(projection, function(b) {
+    var w = extent[1][0] - extent[0][0],
+        h = extent[1][1] - extent[0][1],
+        k = Math.min(w / (b[1][0] - b[0][0]), h / (b[1][1] - b[0][1])),
+        x = +extent[0][0] + (w - k * (b[1][0] + b[0][0])) / 2,
+        y = +extent[0][1] + (h - k * (b[1][1] + b[0][1])) / 2;
+    projection.scale(150 * k).translate([x, y]);
+  }, object);
+}
+
+function fitSize(projection, size, object) {
+  return fitExtent(projection, [[0, 0], size], object);
+}
+
+function fitWidth(projection, width, object) {
+  return fit(projection, function(b) {
+    var w = +width,
+        k = w / (b[1][0] - b[0][0]),
+        x = (w - k * (b[1][0] + b[0][0])) / 2,
+        y = -k * b[0][1];
+    projection.scale(150 * k).translate([x, y]);
+  }, object);
+}
+
+function fitHeight(projection, height, object) {
+  return fit(projection, function(b) {
+    var h = +height,
+        k = h / (b[1][1] - b[0][1]),
+        x = -k * b[0][0],
+        y = (h - k * (b[1][1] + b[0][1])) / 2;
+    projection.scale(150 * k).translate([x, y]);
+  }, object);
+}
+
+var maxDepth = 16, // maximum depth of subdivision
+    cosMinDistance = cos$1(30 * radians); // cos(minimum angular distance)
+
+function resample(project, delta2) {
+  return +delta2 ? resample$1(project, delta2) : resampleNone(project);
+}
+
+function resampleNone(project) {
+  return transformer({
+    point: function(x, y) {
+      x = project(x, y);
+      this.stream.point(x[0], x[1]);
+    }
+  });
+}
+
+function resample$1(project, delta2) {
+
+  function resampleLineTo(x0, y0, lambda0, a0, b0, c0, x1, y1, lambda1, a1, b1, c1, depth, stream) {
+    var dx = x1 - x0,
+        dy = y1 - y0,
+        d2 = dx * dx + dy * dy;
+    if (d2 > 4 * delta2 && depth--) {
+      var a = a0 + a1,
+          b = b0 + b1,
+          c = c0 + c1,
+          m = sqrt(a * a + b * b + c * c),
+          phi2 = asin(c /= m),
+          lambda2 = abs(abs(c) - 1) < epsilon$2 || abs(lambda0 - lambda1) < epsilon$2 ? (lambda0 + lambda1) / 2 : atan2(b, a),
+          p = project(lambda2, phi2),
+          x2 = p[0],
+          y2 = p[1],
+          dx2 = x2 - x0,
+          dy2 = y2 - y0,
+          dz = dy * dx2 - dx * dy2;
+      if (dz * dz / d2 > delta2 // perpendicular projected distance
+          || abs((dx * dx2 + dy * dy2) / d2 - 0.5) > 0.3 // midpoint close to an end
+          || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { // angular distance
+        resampleLineTo(x0, y0, lambda0, a0, b0, c0, x2, y2, lambda2, a /= m, b /= m, c, depth, stream);
+        stream.point(x2, y2);
+        resampleLineTo(x2, y2, lambda2, a, b, c, x1, y1, lambda1, a1, b1, c1, depth, stream);
+      }
+    }
+  }
+  return function(stream) {
+    var lambda00, x00, y00, a00, b00, c00, // first point
+        lambda0, x0, y0, a0, b0, c0; // previous point
+
+    var resampleStream = {
+      point: point,
+      lineStart: lineStart,
+      lineEnd: lineEnd,
+      polygonStart: function() { stream.polygonStart(); resampleStream.lineStart = ringStart; },
+      polygonEnd: function() { stream.polygonEnd(); resampleStream.lineStart = lineStart; }
+    };
+
+    function point(x, y) {
+      x = project(x, y);
+      stream.point(x[0], x[1]);
+    }
+
+    function lineStart() {
+      x0 = NaN;
+      resampleStream.point = linePoint;
+      stream.lineStart();
+    }
+
+    function linePoint(lambda, phi) {
+      var c = cartesian([lambda, phi]), p = project(lambda, phi);
+      resampleLineTo(x0, y0, lambda0, a0, b0, c0, x0 = p[0], y0 = p[1], lambda0 = lambda, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);
+      stream.point(x0, y0);
+    }
+
+    function lineEnd() {
+      resampleStream.point = point;
+      stream.lineEnd();
+    }
+
+    function ringStart() {
+      lineStart();
+      resampleStream.point = ringPoint;
+      resampleStream.lineEnd = ringEnd;
+    }
+
+    function ringPoint(lambda, phi) {
+      linePoint(lambda00 = lambda, phi), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;
+      resampleStream.point = linePoint;
+    }
+
+    function ringEnd() {
+      resampleLineTo(x0, y0, lambda0, a0, b0, c0, x00, y00, lambda00, a00, b00, c00, maxDepth, stream);
+      resampleStream.lineEnd = lineEnd;
+      lineEnd();
+    }
+
+    return resampleStream;
+  };
+}
+
+var transformRadians = transformer({
+  point: function(x, y) {
+    this.stream.point(x * radians, y * radians);
+  }
+});
+
+function transformRotate(rotate) {
+  return transformer({
+    point: function(x, y) {
+      var r = rotate(x, y);
+      return this.stream.point(r[0], r[1]);
+    }
+  });
+}
+
+function scaleTranslate(k, dx, dy) {
+  function transform$$1(x, y) {
+    return [dx + k * x, dy - k * y];
+  }
+  transform$$1.invert = function(x, y) {
+    return [(x - dx) / k, (dy - y) / k];
+  };
+  return transform$$1;
+}
+
+function scaleTranslateRotate(k, dx, dy, alpha) {
+  var cosAlpha = cos$1(alpha),
+      sinAlpha = sin$1(alpha),
+      a = cosAlpha * k,
+      b = sinAlpha * k,
+      ai = cosAlpha / k,
+      bi = sinAlpha / k,
+      ci = (sinAlpha * dy - cosAlpha * dx) / k,
+      fi = (sinAlpha * dx + cosAlpha * dy) / k;
+  function transform$$1(x, y) {
+    return [a * x - b * y + dx, dy - b * x - a * y];
+  }
+  transform$$1.invert = function(x, y) {
+    return [ai * x - bi * y + ci, fi - bi * x - ai * y];
+  };
+  return transform$$1;
+}
+
+function projection(project) {
+  return projectionMutator(function() { return project; })();
+}
+
+function projectionMutator(projectAt) {
+  var project,
+      k = 150, // scale
+      x = 480, y = 250, // translate
+      lambda = 0, phi = 0, // center
+      deltaLambda = 0, deltaPhi = 0, deltaGamma = 0, rotate, // pre-rotate
+      alpha = 0, // post-rotate
+      theta = null, preclip = clipAntimeridian, // pre-clip angle
+      x0 = null, y0, x1, y1, postclip = identity$4, // post-clip extent
+      delta2 = 0.5, // precision
+      projectResample,
+      projectTransform,
+      projectRotateTransform,
+      cache,
+      cacheStream;
+
+  function projection(point) {
+    return projectRotateTransform(point[0] * radians, point[1] * radians);
+  }
+
+  function invert(point) {
+    point = projectRotateTransform.invert(point[0], point[1]);
+    return point && [point[0] * degrees$1, point[1] * degrees$1];
+  }
+
+  projection.stream = function(stream) {
+    return cache && cacheStream === stream ? cache : cache = transformRadians(transformRotate(rotate)(preclip(projectResample(postclip(cacheStream = stream)))));
+  };
+
+  projection.preclip = function(_) {
+    return arguments.length ? (preclip = _, theta = undefined, reset()) : preclip;
+  };
+
+  projection.postclip = function(_) {
+    return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;
+  };
+
+  projection.clipAngle = function(_) {
+    return arguments.length ? (preclip = +_ ? clipCircle(theta = _ * radians) : (theta = null, clipAntimeridian), reset()) : theta * degrees$1;
+  };
+
+  projection.clipExtent = function(_) {
+    return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity$4) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+  };
+
+  projection.scale = function(_) {
+    return arguments.length ? (k = +_, recenter()) : k;
+  };
+
+  projection.translate = function(_) {
+    return arguments.length ? (x = +_[0], y = +_[1], recenter()) : [x, y];
+  };
+
+  projection.center = function(_) {
+    return arguments.length ? (lambda = _[0] % 360 * radians, phi = _[1] % 360 * radians, recenter()) : [lambda * degrees$1, phi * degrees$1];
+  };
+
+  projection.rotate = function(_) {
+    return arguments.length ? (deltaLambda = _[0] % 360 * radians, deltaPhi = _[1] % 360 * radians, deltaGamma = _.length > 2 ? _[2] % 360 * radians : 0, recenter()) : [deltaLambda * degrees$1, deltaPhi * degrees$1, deltaGamma * degrees$1];
+  };
+
+  projection.angle = function(_) {
+    return arguments.length ? (alpha = _ % 360 * radians, recenter()) : alpha * degrees$1;
+  };
+
+  projection.precision = function(_) {
+    return arguments.length ? (projectResample = resample(projectTransform, delta2 = _ * _), reset()) : sqrt(delta2);
+  };
+
+  projection.fitExtent = function(extent, object) {
+    return fitExtent(projection, extent, object);
+  };
+
+  projection.fitSize = function(size, object) {
+    return fitSize(projection, size, object);
+  };
+
+  projection.fitWidth = function(width, object) {
+    return fitWidth(projection, width, object);
+  };
+
+  projection.fitHeight = function(height, object) {
+    return fitHeight(projection, height, object);
+  };
+
+  function recenter() {
+    var center = scaleTranslateRotate(k, 0, 0, alpha).apply(null, project(lambda, phi)),
+        transform$$1 = (alpha ? scaleTranslateRotate : scaleTranslate)(k, x - center[0], y - center[1], alpha);
+    rotate = rotateRadians(deltaLambda, deltaPhi, deltaGamma);
+    projectTransform = compose(project, transform$$1);
+    projectRotateTransform = compose(rotate, projectTransform);
+    projectResample = resample(projectTransform, delta2);
+    return reset();
+  }
+
+  function reset() {
+    cache = cacheStream = null;
+    return projection;
+  }
+
+  return function() {
+    project = projectAt.apply(this, arguments);
+    projection.invert = project.invert && invert;
+    return recenter();
+  };
+}
+
+function conicProjection(projectAt) {
+  var phi0 = 0,
+      phi1 = pi$3 / 3,
+      m = projectionMutator(projectAt),
+      p = m(phi0, phi1);
+
+  p.parallels = function(_) {
+    return arguments.length ? m(phi0 = _[0] * radians, phi1 = _[1] * radians) : [phi0 * degrees$1, phi1 * degrees$1];
+  };
+
+  return p;
+}
+
+function cylindricalEqualAreaRaw(phi0) {
+  var cosPhi0 = cos$1(phi0);
+
+  function forward(lambda, phi) {
+    return [lambda * cosPhi0, sin$1(phi) / cosPhi0];
+  }
+
+  forward.invert = function(x, y) {
+    return [x / cosPhi0, asin(y * cosPhi0)];
+  };
+
+  return forward;
+}
+
+function conicEqualAreaRaw(y0, y1) {
+  var sy0 = sin$1(y0), n = (sy0 + sin$1(y1)) / 2;
+
+  // Are the parallels symmetrical around the Equator?
+  if (abs(n) < epsilon$2) return cylindricalEqualAreaRaw(y0);
+
+  var c = 1 + sy0 * (2 * n - sy0), r0 = sqrt(c) / n;
+
+  function project(x, y) {
+    var r = sqrt(c - 2 * n * sin$1(y)) / n;
+    return [r * sin$1(x *= n), r0 - r * cos$1(x)];
+  }
+
+  project.invert = function(x, y) {
+    var r0y = r0 - y;
+    return [atan2(x, abs(r0y)) / n * sign(r0y), asin((c - (x * x + r0y * r0y) * n * n) / (2 * n))];
+  };
+
+  return project;
+}
+
+function conicEqualArea() {
+  return conicProjection(conicEqualAreaRaw)
+      .scale(155.424)
+      .center([0, 33.6442]);
+}
+
+function albers() {
+  return conicEqualArea()
+      .parallels([29.5, 45.5])
+      .scale(1070)
+      .translate([480, 250])
+      .rotate([96, 0])
+      .center([-0.6, 38.7]);
+}
+
+// The projections must have mutually exclusive clip regions on the sphere,
+// as this will avoid emitting interleaving lines and polygons.
+function multiplex(streams) {
+  var n = streams.length;
+  return {
+    point: function(x, y) { var i = -1; while (++i < n) streams[i].point(x, y); },
+    sphere: function() { var i = -1; while (++i < n) streams[i].sphere(); },
+    lineStart: function() { var i = -1; while (++i < n) streams[i].lineStart(); },
+    lineEnd: function() { var i = -1; while (++i < n) streams[i].lineEnd(); },
+    polygonStart: function() { var i = -1; while (++i < n) streams[i].polygonStart(); },
+    polygonEnd: function() { var i = -1; while (++i < n) streams[i].polygonEnd(); }
+  };
+}
+
+// A composite projection for the United States, configured by default for
+// 960×500. The projection also works quite well at 960×600 if you change the
+// scale to 1285 and adjust the translate accordingly. The set of standard
+// parallels for each region comes from USGS, which is published here:
+// http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers
+function albersUsa() {
+  var cache,
+      cacheStream,
+      lower48 = albers(), lower48Point,
+      alaska = conicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]), alaskaPoint, // EPSG:3338
+      hawaii = conicEqualArea().rotate([157, 0]).center([-3, 19.9]).parallels([8, 18]), hawaiiPoint, // ESRI:102007
+      point, pointStream = {point: function(x, y) { point = [x, y]; }};
+
+  function albersUsa(coordinates) {
+    var x = coordinates[0], y = coordinates[1];
+    return point = null,
+        (lower48Point.point(x, y), point)
+        || (alaskaPoint.point(x, y), point)
+        || (hawaiiPoint.point(x, y), point);
+  }
+
+  albersUsa.invert = function(coordinates) {
+    var k = lower48.scale(),
+        t = lower48.translate(),
+        x = (coordinates[0] - t[0]) / k,
+        y = (coordinates[1] - t[1]) / k;
+    return (y >= 0.120 && y < 0.234 && x >= -0.425 && x < -0.214 ? alaska
+        : y >= 0.166 && y < 0.234 && x >= -0.214 && x < -0.115 ? hawaii
+        : lower48).invert(coordinates);
+  };
+
+  albersUsa.stream = function(stream) {
+    return cache && cacheStream === stream ? cache : cache = multiplex([lower48.stream(cacheStream = stream), alaska.stream(stream), hawaii.stream(stream)]);
+  };
+
+  albersUsa.precision = function(_) {
+    if (!arguments.length) return lower48.precision();
+    lower48.precision(_), alaska.precision(_), hawaii.precision(_);
+    return reset();
+  };
+
+  albersUsa.scale = function(_) {
+    if (!arguments.length) return lower48.scale();
+    lower48.scale(_), alaska.scale(_ * 0.35), hawaii.scale(_);
+    return albersUsa.translate(lower48.translate());
+  };
+
+  albersUsa.translate = function(_) {
+    if (!arguments.length) return lower48.translate();
+    var k = lower48.scale(), x = +_[0], y = +_[1];
+
+    lower48Point = lower48
+        .translate(_)
+        .clipExtent([[x - 0.455 * k, y - 0.238 * k], [x + 0.455 * k, y + 0.238 * k]])
+        .stream(pointStream);
+
+    alaskaPoint = alaska
+        .translate([x - 0.307 * k, y + 0.201 * k])
+        .clipExtent([[x - 0.425 * k + epsilon$2, y + 0.120 * k + epsilon$2], [x - 0.214 * k - epsilon$2, y + 0.234 * k - epsilon$2]])
+        .stream(pointStream);
+
+    hawaiiPoint = hawaii
+        .translate([x - 0.205 * k, y + 0.212 * k])
+        .clipExtent([[x - 0.214 * k + epsilon$2, y + 0.166 * k + epsilon$2], [x - 0.115 * k - epsilon$2, y + 0.234 * k - epsilon$2]])
+        .stream(pointStream);
+
+    return reset();
+  };
+
+  albersUsa.fitExtent = function(extent, object) {
+    return fitExtent(albersUsa, extent, object);
+  };
+
+  albersUsa.fitSize = function(size, object) {
+    return fitSize(albersUsa, size, object);
+  };
+
+  albersUsa.fitWidth = function(width, object) {
+    return fitWidth(albersUsa, width, object);
+  };
+
+  albersUsa.fitHeight = function(height, object) {
+    return fitHeight(albersUsa, height, object);
+  };
+
+  function reset() {
+    cache = cacheStream = null;
+    return albersUsa;
+  }
+
+  return albersUsa.scale(1070);
+}
+
+function azimuthalRaw(scale) {
+  return function(x, y) {
+    var cx = cos$1(x),
+        cy = cos$1(y),
+        k = scale(cx * cy);
+    return [
+      k * cy * sin$1(x),
+      k * sin$1(y)
+    ];
+  }
+}
+
+function azimuthalInvert(angle) {
+  return function(x, y) {
+    var z = sqrt(x * x + y * y),
+        c = angle(z),
+        sc = sin$1(c),
+        cc = cos$1(c);
+    return [
+      atan2(x * sc, z * cc),
+      asin(z && y * sc / z)
+    ];
+  }
+}
+
+var azimuthalEqualAreaRaw = azimuthalRaw(function(cxcy) {
+  return sqrt(2 / (1 + cxcy));
+});
+
+azimuthalEqualAreaRaw.invert = azimuthalInvert(function(z) {
+  return 2 * asin(z / 2);
+});
+
+function azimuthalEqualArea() {
+  return projection(azimuthalEqualAreaRaw)
+      .scale(124.75)
+      .clipAngle(180 - 1e-3);
+}
+
+var azimuthalEquidistantRaw = azimuthalRaw(function(c) {
+  return (c = acos(c)) && c / sin$1(c);
+});
+
+azimuthalEquidistantRaw.invert = azimuthalInvert(function(z) {
+  return z;
+});
+
+function azimuthalEquidistant() {
+  return projection(azimuthalEquidistantRaw)
+      .scale(79.4188)
+      .clipAngle(180 - 1e-3);
+}
+
+function mercatorRaw(lambda, phi) {
+  return [lambda, log(tan((halfPi$2 + phi) / 2))];
+}
+
+mercatorRaw.invert = function(x, y) {
+  return [x, 2 * atan(exp(y)) - halfPi$2];
+};
+
+function mercator() {
+  return mercatorProjection(mercatorRaw)
+      .scale(961 / tau$3);
+}
+
+function mercatorProjection(project) {
+  var m = projection(project),
+      center = m.center,
+      scale = m.scale,
+      translate = m.translate,
+      clipExtent = m.clipExtent,
+      x0 = null, y0, x1, y1; // clip extent
+
+  m.scale = function(_) {
+    return arguments.length ? (scale(_), reclip()) : scale();
+  };
+
+  m.translate = function(_) {
+    return arguments.length ? (translate(_), reclip()) : translate();
+  };
+
+  m.center = function(_) {
+    return arguments.length ? (center(_), reclip()) : center();
+  };
+
+  m.clipExtent = function(_) {
+    return arguments.length ? ((_ == null ? x0 = y0 = x1 = y1 = null : (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1])), reclip()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+  };
+
+  function reclip() {
+    var k = pi$3 * scale(),
+        t = m(rotation(m.rotate()).invert([0, 0]));
+    return clipExtent(x0 == null
+        ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] : project === mercatorRaw
+        ? [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]]
+        : [[x0, Math.max(t[1] - k, y0)], [x1, Math.min(t[1] + k, y1)]]);
+  }
+
+  return reclip();
+}
+
+function tany(y) {
+  return tan((halfPi$2 + y) / 2);
+}
+
+function conicConformalRaw(y0, y1) {
+  var cy0 = cos$1(y0),
+      n = y0 === y1 ? sin$1(y0) : log(cy0 / cos$1(y1)) / log(tany(y1) / tany(y0)),
+      f = cy0 * pow(tany(y0), n) / n;
+
+  if (!n) return mercatorRaw;
+
+  function project(x, y) {
+    if (f > 0) { if (y < -halfPi$2 + epsilon$2) y = -halfPi$2 + epsilon$2; }
+    else { if (y > halfPi$2 - epsilon$2) y = halfPi$2 - epsilon$2; }
+    var r = f / pow(tany(y), n);
+    return [r * sin$1(n * x), f - r * cos$1(n * x)];
+  }
+
+  project.invert = function(x, y) {
+    var fy = f - y, r = sign(n) * sqrt(x * x + fy * fy);
+    return [atan2(x, abs(fy)) / n * sign(fy), 2 * atan(pow(f / r, 1 / n)) - halfPi$2];
+  };
+
+  return project;
+}
+
+function conicConformal() {
+  return conicProjection(conicConformalRaw)
+      .scale(109.5)
+      .parallels([30, 30]);
+}
+
+function equirectangularRaw(lambda, phi) {
+  return [lambda, phi];
+}
+
+equirectangularRaw.invert = equirectangularRaw;
+
+function equirectangular() {
+  return projection(equirectangularRaw)
+      .scale(152.63);
+}
+
+function conicEquidistantRaw(y0, y1) {
+  var cy0 = cos$1(y0),
+      n = y0 === y1 ? sin$1(y0) : (cy0 - cos$1(y1)) / (y1 - y0),
+      g = cy0 / n + y0;
+
+  if (abs(n) < epsilon$2) return equirectangularRaw;
+
+  function project(x, y) {
+    var gy = g - y, nx = n * x;
+    return [gy * sin$1(nx), g - gy * cos$1(nx)];
+  }
+
+  project.invert = function(x, y) {
+    var gy = g - y;
+    return [atan2(x, abs(gy)) / n * sign(gy), g - sign(n) * sqrt(x * x + gy * gy)];
+  };
+
+  return project;
+}
+
+function conicEquidistant() {
+  return conicProjection(conicEquidistantRaw)
+      .scale(131.154)
+      .center([0, 13.9389]);
+}
+
+var A1 = 1.340264,
+    A2 = -0.081106,
+    A3 = 0.000893,
+    A4 = 0.003796,
+    M = sqrt(3) / 2,
+    iterations = 12;
+
+function equalEarthRaw(lambda, phi) {
+  var l = asin(M * sin$1(phi)), l2 = l * l, l6 = l2 * l2 * l2;
+  return [
+    lambda * cos$1(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))),
+    l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))
+  ];
+}
+
+equalEarthRaw.invert = function(x, y) {
+  var l = y, l2 = l * l, l6 = l2 * l2 * l2;
+  for (var i = 0, delta, fy, fpy; i < iterations; ++i) {
+    fy = l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) - y;
+    fpy = A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2);
+    l -= delta = fy / fpy, l2 = l * l, l6 = l2 * l2 * l2;
+    if (abs(delta) < epsilon2$1) break;
+  }
+  return [
+    M * x * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2)) / cos$1(l),
+    asin(sin$1(l) / M)
+  ];
+};
+
+function equalEarth() {
+  return projection(equalEarthRaw)
+      .scale(177.158);
+}
+
+function gnomonicRaw(x, y) {
+  var cy = cos$1(y), k = cos$1(x) * cy;
+  return [cy * sin$1(x) / k, sin$1(y) / k];
+}
+
+gnomonicRaw.invert = azimuthalInvert(atan);
+
+function gnomonic() {
+  return projection(gnomonicRaw)
+      .scale(144.049)
+      .clipAngle(60);
+}
+
+function scaleTranslate$1(kx, ky, tx, ty) {
+  return kx === 1 && ky === 1 && tx === 0 && ty === 0 ? identity$4 : transformer({
+    point: function(x, y) {
+      this.stream.point(x * kx + tx, y * ky + ty);
+    }
+  });
+}
+
+function identity$5() {
+  var k = 1, tx = 0, ty = 0, sx = 1, sy = 1, transform$$1 = identity$4, // scale, translate and reflect
+      x0 = null, y0, x1, y1, // clip extent
+      postclip = identity$4,
+      cache,
+      cacheStream,
+      projection;
+
+  function reset() {
+    cache = cacheStream = null;
+    return projection;
+  }
+
+  return projection = {
+    stream: function(stream) {
+      return cache && cacheStream === stream ? cache : cache = transform$$1(postclip(cacheStream = stream));
+    },
+    postclip: function(_) {
+      return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;
+    },
+    clipExtent: function(_) {
+      return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity$4) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+    },
+    scale: function(_) {
+      return arguments.length ? (transform$$1 = scaleTranslate$1((k = +_) * sx, k * sy, tx, ty), reset()) : k;
+    },
+    translate: function(_) {
+      return arguments.length ? (transform$$1 = scaleTranslate$1(k * sx, k * sy, tx = +_[0], ty = +_[1]), reset()) : [tx, ty];
+    },
+    reflectX: function(_) {
+      return arguments.length ? (transform$$1 = scaleTranslate$1(k * (sx = _ ? -1 : 1), k * sy, tx, ty), reset()) : sx < 0;
+    },
+    reflectY: function(_) {
+      return arguments.length ? (transform$$1 = scaleTranslate$1(k * sx, k * (sy = _ ? -1 : 1), tx, ty), reset()) : sy < 0;
+    },
+    fitExtent: function(extent, object) {
+      return fitExtent(projection, extent, object);
+    },
+    fitSize: function(size, object) {
+      return fitSize(projection, size, object);
+    },
+    fitWidth: function(width, object) {
+      return fitWidth(projection, width, object);
+    },
+    fitHeight: function(height, object) {
+      return fitHeight(projection, height, object);
+    }
+  };
+}
+
+function naturalEarth1Raw(lambda, phi) {
+  var phi2 = phi * phi, phi4 = phi2 * phi2;
+  return [
+    lambda * (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))),
+    phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4)))
+  ];
+}
+
+naturalEarth1Raw.invert = function(x, y) {
+  var phi = y, i = 25, delta;
+  do {
+    var phi2 = phi * phi, phi4 = phi2 * phi2;
+    phi -= delta = (phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) - y) /
+        (1.007226 + phi2 * (0.015085 * 3 + phi4 * (-0.044475 * 7 + 0.028874 * 9 * phi2 - 0.005916 * 11 * phi4)));
+  } while (abs(delta) > epsilon$2 && --i > 0);
+  return [
+    x / (0.8707 + (phi2 = phi * phi) * (-0.131979 + phi2 * (-0.013791 + phi2 * phi2 * phi2 * (0.003971 - 0.001529 * phi2)))),
+    phi
+  ];
+};
+
+function naturalEarth1() {
+  return projection(naturalEarth1Raw)
+      .scale(175.295);
+}
+
+function orthographicRaw(x, y) {
+  return [cos$1(y) * sin$1(x), sin$1(y)];
+}
+
+orthographicRaw.invert = azimuthalInvert(asin);
+
+function orthographic() {
+  return projection(orthographicRaw)
+      .scale(249.5)
+      .clipAngle(90 + epsilon$2);
+}
+
+function stereographicRaw(x, y) {
+  var cy = cos$1(y), k = 1 + cos$1(x) * cy;
+  return [cy * sin$1(x) / k, sin$1(y) / k];
+}
+
+stereographicRaw.invert = azimuthalInvert(function(z) {
+  return 2 * atan(z);
+});
+
+function stereographic() {
+  return projection(stereographicRaw)
+      .scale(250)
+      .clipAngle(142);
+}
+
+function transverseMercatorRaw(lambda, phi) {
+  return [log(tan((halfPi$2 + phi) / 2)), -lambda];
+}
+
+transverseMercatorRaw.invert = function(x, y) {
+  return [-y, 2 * atan(exp(x)) - halfPi$2];
+};
+
+function transverseMercator() {
+  var m = mercatorProjection(transverseMercatorRaw),
+      center = m.center,
+      rotate = m.rotate;
+
+  m.center = function(_) {
+    return arguments.length ? center([-_[1], _[0]]) : (_ = center(), [_[1], -_[0]]);
+  };
+
+  m.rotate = function(_) {
+    return arguments.length ? rotate([_[0], _[1], _.length > 2 ? _[2] + 90 : 90]) : (_ = rotate(), [_[0], _[1], _[2] - 90]);
+  };
+
+  return rotate([0, 0, 90])
+      .scale(159.155);
+}
+
+function defaultSeparation(a, b) {
+  return a.parent === b.parent ? 1 : 2;
+}
+
+function meanX(children) {
+  return children.reduce(meanXReduce, 0) / children.length;
+}
+
+function meanXReduce(x, c) {
+  return x + c.x;
+}
+
+function maxY(children) {
+  return 1 + children.reduce(maxYReduce, 0);
+}
+
+function maxYReduce(y, c) {
+  return Math.max(y, c.y);
+}
+
+function leafLeft(node) {
+  var children;
+  while (children = node.children) node = children[0];
+  return node;
+}
+
+function leafRight(node) {
+  var children;
+  while (children = node.children) node = children[children.length - 1];
+  return node;
+}
+
+function cluster() {
+  var separation = defaultSeparation,
+      dx = 1,
+      dy = 1,
+      nodeSize = false;
+
+  function cluster(root) {
+    var previousNode,
+        x = 0;
+
+    // First walk, computing the initial x & y values.
+    root.eachAfter(function(node) {
+      var children = node.children;
+      if (children) {
+        node.x = meanX(children);
+        node.y = maxY(children);
+      } else {
+        node.x = previousNode ? x += separation(node, previousNode) : 0;
+        node.y = 0;
+        previousNode = node;
+      }
+    });
+
+    var left = leafLeft(root),
+        right = leafRight(root),
+        x0 = left.x - separation(left, right) / 2,
+        x1 = right.x + separation(right, left) / 2;
+
+    // Second walk, normalizing x & y to the desired size.
+    return root.eachAfter(nodeSize ? function(node) {
+      node.x = (node.x - root.x) * dx;
+      node.y = (root.y - node.y) * dy;
+    } : function(node) {
+      node.x = (node.x - x0) / (x1 - x0) * dx;
+      node.y = (1 - (root.y ? node.y / root.y : 1)) * dy;
+    });
+  }
+
+  cluster.separation = function(x) {
+    return arguments.length ? (separation = x, cluster) : separation;
+  };
+
+  cluster.size = function(x) {
+    return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? null : [dx, dy]);
+  };
+
+  cluster.nodeSize = function(x) {
+    return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? [dx, dy] : null);
+  };
+
+  return cluster;
+}
+
+function count(node) {
+  var sum = 0,
+      children = node.children,
+      i = children && children.length;
+  if (!i) sum = 1;
+  else while (--i >= 0) sum += children[i].value;
+  node.value = sum;
+}
+
+function node_count() {
+  return this.eachAfter(count);
+}
+
+function node_each(callback) {
+  var node = this, current, next = [node], children, i, n;
+  do {
+    current = next.reverse(), next = [];
+    while (node = current.pop()) {
+      callback(node), children = node.children;
+      if (children) for (i = 0, n = children.length; i < n; ++i) {
+        next.push(children[i]);
+      }
+    }
+  } while (next.length);
+  return this;
+}
+
+function node_eachBefore(callback) {
+  var node = this, nodes = [node], children, i;
+  while (node = nodes.pop()) {
+    callback(node), children = node.children;
+    if (children) for (i = children.length - 1; i >= 0; --i) {
+      nodes.push(children[i]);
+    }
+  }
+  return this;
+}
+
+function node_eachAfter(callback) {
+  var node = this, nodes = [node], next = [], children, i, n;
+  while (node = nodes.pop()) {
+    next.push(node), children = node.children;
+    if (children) for (i = 0, n = children.length; i < n; ++i) {
+      nodes.push(children[i]);
+    }
+  }
+  while (node = next.pop()) {
+    callback(node);
+  }
+  return this;
+}
+
+function node_sum(value) {
+  return this.eachAfter(function(node) {
+    var sum = +value(node.data) || 0,
+        children = node.children,
+        i = children && children.length;
+    while (--i >= 0) sum += children[i].value;
+    node.value = sum;
+  });
+}
+
+function node_sort(compare) {
+  return this.eachBefore(function(node) {
+    if (node.children) {
+      node.children.sort(compare);
+    }
+  });
+}
+
+function node_path(end) {
+  var start = this,
+      ancestor = leastCommonAncestor(start, end),
+      nodes = [start];
+  while (start !== ancestor) {
+    start = start.parent;
+    nodes.push(start);
+  }
+  var k = nodes.length;
+  while (end !== ancestor) {
+    nodes.splice(k, 0, end);
+    end = end.parent;
+  }
+  return nodes;
+}
+
+function leastCommonAncestor(a, b) {
+  if (a === b) return a;
+  var aNodes = a.ancestors(),
+      bNodes = b.ancestors(),
+      c = null;
+  a = aNodes.pop();
+  b = bNodes.pop();
+  while (a === b) {
+    c = a;
+    a = aNodes.pop();
+    b = bNodes.pop();
+  }
+  return c;
+}
+
+function node_ancestors() {
+  var node = this, nodes = [node];
+  while (node = node.parent) {
+    nodes.push(node);
+  }
+  return nodes;
+}
+
+function node_descendants() {
+  var nodes = [];
+  this.each(function(node) {
+    nodes.push(node);
+  });
+  return nodes;
+}
+
+function node_leaves() {
+  var leaves = [];
+  this.eachBefore(function(node) {
+    if (!node.children) {
+      leaves.push(node);
+    }
+  });
+  return leaves;
+}
+
+function node_links() {
+  var root = this, links = [];
+  root.each(function(node) {
+    if (node !== root) { // Don’t include the root’s parent, if any.
+      links.push({source: node.parent, target: node});
+    }
+  });
+  return links;
+}
+
+function hierarchy(data, children) {
+  var root = new Node(data),
+      valued = +data.value && (root.value = data.value),
+      node,
+      nodes = [root],
+      child,
+      childs,
+      i,
+      n;
+
+  if (children == null) children = defaultChildren;
+
+  while (node = nodes.pop()) {
+    if (valued) node.value = +node.data.value;
+    if ((childs = children(node.data)) && (n = childs.length)) {
+      node.children = new Array(n);
+      for (i = n - 1; i >= 0; --i) {
+        nodes.push(child = node.children[i] = new Node(childs[i]));
+        child.parent = node;
+        child.depth = node.depth + 1;
+      }
+    }
+  }
+
+  return root.eachBefore(computeHeight);
+}
+
+function node_copy() {
+  return hierarchy(this).eachBefore(copyData);
+}
+
+function defaultChildren(d) {
+  return d.children;
+}
+
+function copyData(node) {
+  node.data = node.data.data;
+}
+
+function computeHeight(node) {
+  var height = 0;
+  do node.height = height;
+  while ((node = node.parent) && (node.height < ++height));
+}
+
+function Node(data) {
+  this.data = data;
+  this.depth =
+  this.height = 0;
+  this.parent = null;
+}
+
+Node.prototype = hierarchy.prototype = {
+  constructor: Node,
+  count: node_count,
+  each: node_each,
+  eachAfter: node_eachAfter,
+  eachBefore: node_eachBefore,
+  sum: node_sum,
+  sort: node_sort,
+  path: node_path,
+  ancestors: node_ancestors,
+  descendants: node_descendants,
+  leaves: node_leaves,
+  links: node_links,
+  copy: node_copy
+};
+
+var slice$4 = Array.prototype.slice;
+
+function shuffle$1(array) {
+  var m = array.length,
+      t,
+      i;
+
+  while (m) {
+    i = Math.random() * m-- | 0;
+    t = array[m];
+    array[m] = array[i];
+    array[i] = t;
+  }
+
+  return array;
+}
+
+function enclose(circles) {
+  var i = 0, n = (circles = shuffle$1(slice$4.call(circles))).length, B = [], p, e;
+
+  while (i < n) {
+    p = circles[i];
+    if (e && enclosesWeak(e, p)) ++i;
+    else e = encloseBasis(B = extendBasis(B, p)), i = 0;
+  }
+
+  return e;
+}
+
+function extendBasis(B, p) {
+  var i, j;
+
+  if (enclosesWeakAll(p, B)) return [p];
+
+  // If we get here then B must have at least one element.
+  for (i = 0; i < B.length; ++i) {
+    if (enclosesNot(p, B[i])
+        && enclosesWeakAll(encloseBasis2(B[i], p), B)) {
+      return [B[i], p];
+    }
+  }
+
+  // If we get here then B must have at least two elements.
+  for (i = 0; i < B.length - 1; ++i) {
+    for (j = i + 1; j < B.length; ++j) {
+      if (enclosesNot(encloseBasis2(B[i], B[j]), p)
+          && enclosesNot(encloseBasis2(B[i], p), B[j])
+          && enclosesNot(encloseBasis2(B[j], p), B[i])
+          && enclosesWeakAll(encloseBasis3(B[i], B[j], p), B)) {
+        return [B[i], B[j], p];
+      }
+    }
+  }
+
+  // If we get here then something is very wrong.
+  throw new Error;
+}
+
+function enclosesNot(a, b) {
+  var dr = a.r - b.r, dx = b.x - a.x, dy = b.y - a.y;
+  return dr < 0 || dr * dr < dx * dx + dy * dy;
+}
+
+function enclosesWeak(a, b) {
+  var dr = a.r - b.r + 1e-6, dx = b.x - a.x, dy = b.y - a.y;
+  return dr > 0 && dr * dr > dx * dx + dy * dy;
+}
+
+function enclosesWeakAll(a, B) {
+  for (var i = 0; i < B.length; ++i) {
+    if (!enclosesWeak(a, B[i])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function encloseBasis(B) {
+  switch (B.length) {
+    case 1: return encloseBasis1(B[0]);
+    case 2: return encloseBasis2(B[0], B[1]);
+    case 3: return encloseBasis3(B[0], B[1], B[2]);
+  }
+}
+
+function encloseBasis1(a) {
+  return {
+    x: a.x,
+    y: a.y,
+    r: a.r
+  };
+}
+
+function encloseBasis2(a, b) {
+  var x1 = a.x, y1 = a.y, r1 = a.r,
+      x2 = b.x, y2 = b.y, r2 = b.r,
+      x21 = x2 - x1, y21 = y2 - y1, r21 = r2 - r1,
+      l = Math.sqrt(x21 * x21 + y21 * y21);
+  return {
+    x: (x1 + x2 + x21 / l * r21) / 2,
+    y: (y1 + y2 + y21 / l * r21) / 2,
+    r: (l + r1 + r2) / 2
+  };
+}
+
+function encloseBasis3(a, b, c) {
+  var x1 = a.x, y1 = a.y, r1 = a.r,
+      x2 = b.x, y2 = b.y, r2 = b.r,
+      x3 = c.x, y3 = c.y, r3 = c.r,
+      a2 = x1 - x2,
+      a3 = x1 - x3,
+      b2 = y1 - y2,
+      b3 = y1 - y3,
+      c2 = r2 - r1,
+      c3 = r3 - r1,
+      d1 = x1 * x1 + y1 * y1 - r1 * r1,
+      d2 = d1 - x2 * x2 - y2 * y2 + r2 * r2,
+      d3 = d1 - x3 * x3 - y3 * y3 + r3 * r3,
+      ab = a3 * b2 - a2 * b3,
+      xa = (b2 * d3 - b3 * d2) / (ab * 2) - x1,
+      xb = (b3 * c2 - b2 * c3) / ab,
+      ya = (a3 * d2 - a2 * d3) / (ab * 2) - y1,
+      yb = (a2 * c3 - a3 * c2) / ab,
+      A = xb * xb + yb * yb - 1,
+      B = 2 * (r1 + xa * xb + ya * yb),
+      C = xa * xa + ya * ya - r1 * r1,
+      r = -(A ? (B + Math.sqrt(B * B - 4 * A * C)) / (2 * A) : C / B);
+  return {
+    x: x1 + xa + xb * r,
+    y: y1 + ya + yb * r,
+    r: r
+  };
+}
+
+function place(b, a, c) {
+  var dx = b.x - a.x, x, a2,
+      dy = b.y - a.y, y, b2,
+      d2 = dx * dx + dy * dy;
+  if (d2) {
+    a2 = a.r + c.r, a2 *= a2;
+    b2 = b.r + c.r, b2 *= b2;
+    if (a2 > b2) {
+      x = (d2 + b2 - a2) / (2 * d2);
+      y = Math.sqrt(Math.max(0, b2 / d2 - x * x));
+      c.x = b.x - x * dx - y * dy;
+      c.y = b.y - x * dy + y * dx;
+    } else {
+      x = (d2 + a2 - b2) / (2 * d2);
+      y = Math.sqrt(Math.max(0, a2 / d2 - x * x));
+      c.x = a.x + x * dx - y * dy;
+      c.y = a.y + x * dy + y * dx;
+    }
+  } else {
+    c.x = a.x + c.r;
+    c.y = a.y;
+  }
+}
+
+function intersects(a, b) {
+  var dr = a.r + b.r - 1e-6, dx = b.x - a.x, dy = b.y - a.y;
+  return dr > 0 && dr * dr > dx * dx + dy * dy;
+}
+
+function score(node) {
+  var a = node._,
+      b = node.next._,
+      ab = a.r + b.r,
+      dx = (a.x * b.r + b.x * a.r) / ab,
+      dy = (a.y * b.r + b.y * a.r) / ab;
+  return dx * dx + dy * dy;
+}
+
+function Node$1(circle) {
+  this._ = circle;
+  this.next = null;
+  this.previous = null;
+}
+
+function packEnclose(circles) {
+  if (!(n = circles.length)) return 0;
+
+  var a, b, c, n, aa, ca, i, j, k, sj, sk;
+
+  // Place the first circle.
+  a = circles[0], a.x = 0, a.y = 0;
+  if (!(n > 1)) return a.r;
+
+  // Place the second circle.
+  b = circles[1], a.x = -b.r, b.x = a.r, b.y = 0;
+  if (!(n > 2)) return a.r + b.r;
+
+  // Place the third circle.
+  place(b, a, c = circles[2]);
+
+  // Initialize the front-chain using the first three circles a, b and c.
+  a = new Node$1(a), b = new Node$1(b), c = new Node$1(c);
+  a.next = c.previous = b;
+  b.next = a.previous = c;
+  c.next = b.previous = a;
+
+  // Attempt to place each remaining circle…
+  pack: for (i = 3; i < n; ++i) {
+    place(a._, b._, c = circles[i]), c = new Node$1(c);
+
+    // Find the closest intersecting circle on the front-chain, if any.
+    // “Closeness” is determined by linear distance along the front-chain.
+    // “Ahead” or “behind” is likewise determined by linear distance.
+    j = b.next, k = a.previous, sj = b._.r, sk = a._.r;
+    do {
+      if (sj <= sk) {
+        if (intersects(j._, c._)) {
+          b = j, a.next = b, b.previous = a, --i;
+          continue pack;
+        }
+        sj += j._.r, j = j.next;
+      } else {
+        if (intersects(k._, c._)) {
+          a = k, a.next = b, b.previous = a, --i;
+          continue pack;
+        }
+        sk += k._.r, k = k.previous;
+      }
+    } while (j !== k.next);
+
+    // Success! Insert the new circle c between a and b.
+    c.previous = a, c.next = b, a.next = b.previous = b = c;
+
+    // Compute the new closest circle pair to the centroid.
+    aa = score(a);
+    while ((c = c.next) !== b) {
+      if ((ca = score(c)) < aa) {
+        a = c, aa = ca;
+      }
+    }
+    b = a.next;
+  }
+
+  // Compute the enclosing circle of the front chain.
+  a = [b._], c = b; while ((c = c.next) !== b) a.push(c._); c = enclose(a);
+
+  // Translate the circles to put the enclosing circle around the origin.
+  for (i = 0; i < n; ++i) a = circles[i], a.x -= c.x, a.y -= c.y;
+
+  return c.r;
+}
+
+function siblings(circles) {
+  packEnclose(circles);
+  return circles;
+}
+
+function optional(f) {
+  return f == null ? null : required(f);
+}
+
+function required(f) {
+  if (typeof f !== "function") throw new Error;
+  return f;
+}
+
+function constantZero() {
+  return 0;
+}
+
+function constant$9(x) {
+  return function() {
+    return x;
+  };
+}
+
+function defaultRadius$1(d) {
+  return Math.sqrt(d.value);
+}
+
+function index$2() {
+  var radius = null,
+      dx = 1,
+      dy = 1,
+      padding = constantZero;
+
+  function pack(root) {
+    root.x = dx / 2, root.y = dy / 2;
+    if (radius) {
+      root.eachBefore(radiusLeaf(radius))
+          .eachAfter(packChildren(padding, 0.5))
+          .eachBefore(translateChild(1));
+    } else {
+      root.eachBefore(radiusLeaf(defaultRadius$1))
+          .eachAfter(packChildren(constantZero, 1))
+          .eachAfter(packChildren(padding, root.r / Math.min(dx, dy)))
+          .eachBefore(translateChild(Math.min(dx, dy) / (2 * root.r)));
+    }
+    return root;
+  }
+
+  pack.radius = function(x) {
+    return arguments.length ? (radius = optional(x), pack) : radius;
+  };
+
+  pack.size = function(x) {
+    return arguments.length ? (dx = +x[0], dy = +x[1], pack) : [dx, dy];
+  };
+
+  pack.padding = function(x) {
+    return arguments.length ? (padding = typeof x === "function" ? x : constant$9(+x), pack) : padding;
+  };
+
+  return pack;
+}
+
+function radiusLeaf(radius) {
+  return function(node) {
+    if (!node.children) {
+      node.r = Math.max(0, +radius(node) || 0);
+    }
+  };
+}
+
+function packChildren(padding, k) {
+  return function(node) {
+    if (children = node.children) {
+      var children,
+          i,
+          n = children.length,
+          r = padding(node) * k || 0,
+          e;
+
+      if (r) for (i = 0; i < n; ++i) children[i].r += r;
+      e = packEnclose(children);
+      if (r) for (i = 0; i < n; ++i) children[i].r -= r;
+      node.r = e + r;
+    }
+  };
+}
+
+function translateChild(k) {
+  return function(node) {
+    var parent = node.parent;
+    node.r *= k;
+    if (parent) {
+      node.x = parent.x + k * node.x;
+      node.y = parent.y + k * node.y;
+    }
+  };
+}
+
+function roundNode(node) {
+  node.x0 = Math.round(node.x0);
+  node.y0 = Math.round(node.y0);
+  node.x1 = Math.round(node.x1);
+  node.y1 = Math.round(node.y1);
+}
+
+function treemapDice(parent, x0, y0, x1, y1) {
+  var nodes = parent.children,
+      node,
+      i = -1,
+      n = nodes.length,
+      k = parent.value && (x1 - x0) / parent.value;
+
+  while (++i < n) {
+    node = nodes[i], node.y0 = y0, node.y1 = y1;
+    node.x0 = x0, node.x1 = x0 += node.value * k;
+  }
+}
+
+function partition() {
+  var dx = 1,
+      dy = 1,
+      padding = 0,
+      round = false;
+
+  function partition(root) {
+    var n = root.height + 1;
+    root.x0 =
+    root.y0 = padding;
+    root.x1 = dx;
+    root.y1 = dy / n;
+    root.eachBefore(positionNode(dy, n));
+    if (round) root.eachBefore(roundNode);
+    return root;
+  }
+
+  function positionNode(dy, n) {
+    return function(node) {
+      if (node.children) {
+        treemapDice(node, node.x0, dy * (node.depth + 1) / n, node.x1, dy * (node.depth + 2) / n);
+      }
+      var x0 = node.x0,
+          y0 = node.y0,
+          x1 = node.x1 - padding,
+          y1 = node.y1 - padding;
+      if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+      if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+      node.x0 = x0;
+      node.y0 = y0;
+      node.x1 = x1;
+      node.y1 = y1;
+    };
+  }
+
+  partition.round = function(x) {
+    return arguments.length ? (round = !!x, partition) : round;
+  };
+
+  partition.size = function(x) {
+    return arguments.length ? (dx = +x[0], dy = +x[1], partition) : [dx, dy];
+  };
+
+  partition.padding = function(x) {
+    return arguments.length ? (padding = +x, partition) : padding;
+  };
+
+  return partition;
+}
+
+var keyPrefix$1 = "$", // Protect against keys like “__proto__”.
+    preroot = {depth: -1},
+    ambiguous = {};
+
+function defaultId(d) {
+  return d.id;
+}
+
+function defaultParentId(d) {
+  return d.parentId;
+}
+
+function stratify() {
+  var id = defaultId,
+      parentId = defaultParentId;
+
+  function stratify(data) {
+    var d,
+        i,
+        n = data.length,
+        root,
+        parent,
+        node,
+        nodes = new Array(n),
+        nodeId,
+        nodeKey,
+        nodeByKey = {};
+
+    for (i = 0; i < n; ++i) {
+      d = data[i], node = nodes[i] = new Node(d);
+      if ((nodeId = id(d, i, data)) != null && (nodeId += "")) {
+        nodeKey = keyPrefix$1 + (node.id = nodeId);
+        nodeByKey[nodeKey] = nodeKey in nodeByKey ? ambiguous : node;
+      }
+    }
+
+    for (i = 0; i < n; ++i) {
+      node = nodes[i], nodeId = parentId(data[i], i, data);
+      if (nodeId == null || !(nodeId += "")) {
+        if (root) throw new Error("multiple roots");
+        root = node;
+      } else {
+        parent = nodeByKey[keyPrefix$1 + nodeId];
+        if (!parent) throw new Error("missing: " + nodeId);
+        if (parent === ambiguous) throw new Error("ambiguous: " + nodeId);
+        if (parent.children) parent.children.push(node);
+        else parent.children = [node];
+        node.parent = parent;
+      }
+    }
+
+    if (!root) throw new Error("no root");
+    root.parent = preroot;
+    root.eachBefore(function(node) { node.depth = node.parent.depth + 1; --n; }).eachBefore(computeHeight);
+    root.parent = null;
+    if (n > 0) throw new Error("cycle");
+
+    return root;
+  }
+
+  stratify.id = function(x) {
+    return arguments.length ? (id = required(x), stratify) : id;
+  };
+
+  stratify.parentId = function(x) {
+    return arguments.length ? (parentId = required(x), stratify) : parentId;
+  };
+
+  return stratify;
+}
+
+function defaultSeparation$1(a, b) {
+  return a.parent === b.parent ? 1 : 2;
+}
+
+// function radialSeparation(a, b) {
+//   return (a.parent === b.parent ? 1 : 2) / a.depth;
+// }
+
+// This function is used to traverse the left contour of a subtree (or
+// subforest). It returns the successor of v on this contour. This successor is
+// either given by the leftmost child of v or by the thread of v. The function
+// returns null if and only if v is on the highest level of its subtree.
+function nextLeft(v) {
+  var children = v.children;
+  return children ? children[0] : v.t;
+}
+
+// This function works analogously to nextLeft.
+function nextRight(v) {
+  var children = v.children;
+  return children ? children[children.length - 1] : v.t;
+}
+
+// Shifts the current subtree rooted at w+. This is done by increasing
+// prelim(w+) and mod(w+) by shift.
+function moveSubtree(wm, wp, shift) {
+  var change = shift / (wp.i - wm.i);
+  wp.c -= change;
+  wp.s += shift;
+  wm.c += change;
+  wp.z += shift;
+  wp.m += shift;
+}
+
+// All other shifts, applied to the smaller subtrees between w- and w+, are
+// performed by this function. To prepare the shifts, we have to adjust
+// change(w+), shift(w+), and change(w-).
+function executeShifts(v) {
+  var shift = 0,
+      change = 0,
+      children = v.children,
+      i = children.length,
+      w;
+  while (--i >= 0) {
+    w = children[i];
+    w.z += shift;
+    w.m += shift;
+    shift += w.s + (change += w.c);
+  }
+}
+
+// If vi-’s ancestor is a sibling of v, returns vi-’s ancestor. Otherwise,
+// returns the specified (default) ancestor.
+function nextAncestor(vim, v, ancestor) {
+  return vim.a.parent === v.parent ? vim.a : ancestor;
+}
+
+function TreeNode(node, i) {
+  this._ = node;
+  this.parent = null;
+  this.children = null;
+  this.A = null; // default ancestor
+  this.a = this; // ancestor
+  this.z = 0; // prelim
+  this.m = 0; // mod
+  this.c = 0; // change
+  this.s = 0; // shift
+  this.t = null; // thread
+  this.i = i; // number
+}
+
+TreeNode.prototype = Object.create(Node.prototype);
+
+function treeRoot(root) {
+  var tree = new TreeNode(root, 0),
+      node,
+      nodes = [tree],
+      child,
+      children,
+      i,
+      n;
+
+  while (node = nodes.pop()) {
+    if (children = node._.children) {
+      node.children = new Array(n = children.length);
+      for (i = n - 1; i >= 0; --i) {
+        nodes.push(child = node.children[i] = new TreeNode(children[i], i));
+        child.parent = node;
+      }
+    }
+  }
+
+  (tree.parent = new TreeNode(null, 0)).children = [tree];
+  return tree;
+}
+
+// Node-link tree diagram using the Reingold-Tilford "tidy" algorithm
+function tree() {
+  var separation = defaultSeparation$1,
+      dx = 1,
+      dy = 1,
+      nodeSize = null;
+
+  function tree(root) {
+    var t = treeRoot(root);
+
+    // Compute the layout using Buchheim et al.’s algorithm.
+    t.eachAfter(firstWalk), t.parent.m = -t.z;
+    t.eachBefore(secondWalk);
+
+    // If a fixed node size is specified, scale x and y.
+    if (nodeSize) root.eachBefore(sizeNode);
+
+    // If a fixed tree size is specified, scale x and y based on the extent.
+    // Compute the left-most, right-most, and depth-most nodes for extents.
+    else {
+      var left = root,
+          right = root,
+          bottom = root;
+      root.eachBefore(function(node) {
+        if (node.x < left.x) left = node;
+        if (node.x > right.x) right = node;
+        if (node.depth > bottom.depth) bottom = node;
+      });
+      var s = left === right ? 1 : separation(left, right) / 2,
+          tx = s - left.x,
+          kx = dx / (right.x + s + tx),
+          ky = dy / (bottom.depth || 1);
+      root.eachBefore(function(node) {
+        node.x = (node.x + tx) * kx;
+        node.y = node.depth * ky;
+      });
+    }
+
+    return root;
+  }
+
+  // Computes a preliminary x-coordinate for v. Before that, FIRST WALK is
+  // applied recursively to the children of v, as well as the function
+  // APPORTION. After spacing out the children by calling EXECUTE SHIFTS, the
+  // node v is placed to the midpoint of its outermost children.
+  function firstWalk(v) {
+    var children = v.children,
+        siblings = v.parent.children,
+        w = v.i ? siblings[v.i - 1] : null;
+    if (children) {
+      executeShifts(v);
+      var midpoint = (children[0].z + children[children.length - 1].z) / 2;
+      if (w) {
+        v.z = w.z + separation(v._, w._);
+        v.m = v.z - midpoint;
+      } else {
+        v.z = midpoint;
+      }
+    } else if (w) {
+      v.z = w.z + separation(v._, w._);
+    }
+    v.parent.A = apportion(v, w, v.parent.A || siblings[0]);
+  }
+
+  // Computes all real x-coordinates by summing up the modifiers recursively.
+  function secondWalk(v) {
+    v._.x = v.z + v.parent.m;
+    v.m += v.parent.m;
+  }
+
+  // The core of the algorithm. Here, a new subtree is combined with the
+  // previous subtrees. Threads are used to traverse the inside and outside
+  // contours of the left and right subtree up to the highest common level. The
+  // vertices used for the traversals are vi+, vi-, vo-, and vo+, where the
+  // superscript o means outside and i means inside, the subscript - means left
+  // subtree and + means right subtree. For summing up the modifiers along the
+  // contour, we use respective variables si+, si-, so-, and so+. Whenever two
+  // nodes of the inside contours conflict, we compute the left one of the
+  // greatest uncommon ancestors using the function ANCESTOR and call MOVE
+  // SUBTREE to shift the subtree and prepare the shifts of smaller subtrees.
+  // Finally, we add a new thread (if necessary).
+  function apportion(v, w, ancestor) {
+    if (w) {
+      var vip = v,
+          vop = v,
+          vim = w,
+          vom = vip.parent.children[0],
+          sip = vip.m,
+          sop = vop.m,
+          sim = vim.m,
+          som = vom.m,
+          shift;
+      while (vim = nextRight(vim), vip = nextLeft(vip), vim && vip) {
+        vom = nextLeft(vom);
+        vop = nextRight(vop);
+        vop.a = v;
+        shift = vim.z + sim - vip.z - sip + separation(vim._, vip._);
+        if (shift > 0) {
+          moveSubtree(nextAncestor(vim, v, ancestor), v, shift);
+          sip += shift;
+          sop += shift;
+        }
+        sim += vim.m;
+        sip += vip.m;
+        som += vom.m;
+        sop += vop.m;
+      }
+      if (vim && !nextRight(vop)) {
+        vop.t = vim;
+        vop.m += sim - sop;
+      }
+      if (vip && !nextLeft(vom)) {
+        vom.t = vip;
+        vom.m += sip - som;
+        ancestor = v;
+      }
+    }
+    return ancestor;
+  }
+
+  function sizeNode(node) {
+    node.x *= dx;
+    node.y = node.depth * dy;
+  }
+
+  tree.separation = function(x) {
+    return arguments.length ? (separation = x, tree) : separation;
+  };
+
+  tree.size = function(x) {
+    return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], tree) : (nodeSize ? null : [dx, dy]);
+  };
+
+  tree.nodeSize = function(x) {
+    return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], tree) : (nodeSize ? [dx, dy] : null);
+  };
+
+  return tree;
+}
+
+function treemapSlice(parent, x0, y0, x1, y1) {
+  var nodes = parent.children,
+      node,
+      i = -1,
+      n = nodes.length,
+      k = parent.value && (y1 - y0) / parent.value;
+
+  while (++i < n) {
+    node = nodes[i], node.x0 = x0, node.x1 = x1;
+    node.y0 = y0, node.y1 = y0 += node.value * k;
+  }
+}
+
+var phi = (1 + Math.sqrt(5)) / 2;
+
+function squarifyRatio(ratio, parent, x0, y0, x1, y1) {
+  var rows = [],
+      nodes = parent.children,
+      row,
+      nodeValue,
+      i0 = 0,
+      i1 = 0,
+      n = nodes.length,
+      dx, dy,
+      value = parent.value,
+      sumValue,
+      minValue,
+      maxValue,
+      newRatio,
+      minRatio,
+      alpha,
+      beta;
+
+  while (i0 < n) {
+    dx = x1 - x0, dy = y1 - y0;
+
+    // Find the next non-empty node.
+    do sumValue = nodes[i1++].value; while (!sumValue && i1 < n);
+    minValue = maxValue = sumValue;
+    alpha = Math.max(dy / dx, dx / dy) / (value * ratio);
+    beta = sumValue * sumValue * alpha;
+    minRatio = Math.max(maxValue / beta, beta / minValue);
+
+    // Keep adding nodes while the aspect ratio maintains or improves.
+    for (; i1 < n; ++i1) {
+      sumValue += nodeValue = nodes[i1].value;
+      if (nodeValue < minValue) minValue = nodeValue;
+      if (nodeValue > maxValue) maxValue = nodeValue;
+      beta = sumValue * sumValue * alpha;
+      newRatio = Math.max(maxValue / beta, beta / minValue);
+      if (newRatio > minRatio) { sumValue -= nodeValue; break; }
+      minRatio = newRatio;
+    }
+
+    // Position and record the row orientation.
+    rows.push(row = {value: sumValue, dice: dx < dy, children: nodes.slice(i0, i1)});
+    if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += dy * sumValue / value : y1);
+    else treemapSlice(row, x0, y0, value ? x0 += dx * sumValue / value : x1, y1);
+    value -= sumValue, i0 = i1;
+  }
+
+  return rows;
+}
+
+var squarify = (function custom(ratio) {
+
+  function squarify(parent, x0, y0, x1, y1) {
+    squarifyRatio(ratio, parent, x0, y0, x1, y1);
+  }
+
+  squarify.ratio = function(x) {
+    return custom((x = +x) > 1 ? x : 1);
+  };
+
+  return squarify;
+})(phi);
+
+function index$3() {
+  var tile = squarify,
+      round = false,
+      dx = 1,
+      dy = 1,
+      paddingStack = [0],
+      paddingInner = constantZero,
+      paddingTop = constantZero,
+      paddingRight = constantZero,
+      paddingBottom = constantZero,
+      paddingLeft = constantZero;
+
+  function treemap(root) {
+    root.x0 =
+    root.y0 = 0;
+    root.x1 = dx;
+    root.y1 = dy;
+    root.eachBefore(positionNode);
+    paddingStack = [0];
+    if (round) root.eachBefore(roundNode);
+    return root;
+  }
+
+  function positionNode(node) {
+    var p = paddingStack[node.depth],
+        x0 = node.x0 + p,
+        y0 = node.y0 + p,
+        x1 = node.x1 - p,
+        y1 = node.y1 - p;
+    if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+    if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+    node.x0 = x0;
+    node.y0 = y0;
+    node.x1 = x1;
+    node.y1 = y1;
+    if (node.children) {
+      p = paddingStack[node.depth + 1] = paddingInner(node) / 2;
+      x0 += paddingLeft(node) - p;
+      y0 += paddingTop(node) - p;
+      x1 -= paddingRight(node) - p;
+      y1 -= paddingBottom(node) - p;
+      if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+      if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+      tile(node, x0, y0, x1, y1);
+    }
+  }
+
+  treemap.round = function(x) {
+    return arguments.length ? (round = !!x, treemap) : round;
+  };
+
+  treemap.size = function(x) {
+    return arguments.length ? (dx = +x[0], dy = +x[1], treemap) : [dx, dy];
+  };
+
+  treemap.tile = function(x) {
+    return arguments.length ? (tile = required(x), treemap) : tile;
+  };
+
+  treemap.padding = function(x) {
+    return arguments.length ? treemap.paddingInner(x).paddingOuter(x) : treemap.paddingInner();
+  };
+
+  treemap.paddingInner = function(x) {
+    return arguments.length ? (paddingInner = typeof x === "function" ? x : constant$9(+x), treemap) : paddingInner;
+  };
+
+  treemap.paddingOuter = function(x) {
+    return arguments.length ? treemap.paddingTop(x).paddingRight(x).paddingBottom(x).paddingLeft(x) : treemap.paddingTop();
+  };
+
+  treemap.paddingTop = function(x) {
+    return arguments.length ? (paddingTop = typeof x === "function" ? x : constant$9(+x), treemap) : paddingTop;
+  };
+
+  treemap.paddingRight = function(x) {
+    return arguments.length ? (paddingRight = typeof x === "function" ? x : constant$9(+x), treemap) : paddingRight;
+  };
+
+  treemap.paddingBottom = function(x) {
+    return arguments.length ? (paddingBottom = typeof x === "function" ? x : constant$9(+x), treemap) : paddingBottom;
+  };
+
+  treemap.paddingLeft = function(x) {
+    return arguments.length ? (paddingLeft = typeof x === "function" ? x : constant$9(+x), treemap) : paddingLeft;
+  };
+
+  return treemap;
+}
+
+function binary(parent, x0, y0, x1, y1) {
+  var nodes = parent.children,
+      i, n = nodes.length,
+      sum, sums = new Array(n + 1);
+
+  for (sums[0] = sum = i = 0; i < n; ++i) {
+    sums[i + 1] = sum += nodes[i].value;
+  }
+
+  partition(0, n, parent.value, x0, y0, x1, y1);
+
+  function partition(i, j, value, x0, y0, x1, y1) {
+    if (i >= j - 1) {
+      var node = nodes[i];
+      node.x0 = x0, node.y0 = y0;
+      node.x1 = x1, node.y1 = y1;
+      return;
+    }
+
+    var valueOffset = sums[i],
+        valueTarget = (value / 2) + valueOffset,
+        k = i + 1,
+        hi = j - 1;
+
+    while (k < hi) {
+      var mid = k + hi >>> 1;
+      if (sums[mid] < valueTarget) k = mid + 1;
+      else hi = mid;
+    }
+
+    if ((valueTarget - sums[k - 1]) < (sums[k] - valueTarget) && i + 1 < k) --k;
+
+    var valueLeft = sums[k] - valueOffset,
+        valueRight = value - valueLeft;
+
+    if ((x1 - x0) > (y1 - y0)) {
+      var xk = (x0 * valueRight + x1 * valueLeft) / value;
+      partition(i, k, valueLeft, x0, y0, xk, y1);
+      partition(k, j, valueRight, xk, y0, x1, y1);
+    } else {
+      var yk = (y0 * valueRight + y1 * valueLeft) / value;
+      partition(i, k, valueLeft, x0, y0, x1, yk);
+      partition(k, j, valueRight, x0, yk, x1, y1);
+    }
+  }
+}
+
+function sliceDice(parent, x0, y0, x1, y1) {
+  (parent.depth & 1 ? treemapSlice : treemapDice)(parent, x0, y0, x1, y1);
+}
+
+var resquarify = (function custom(ratio) {
+
+  function resquarify(parent, x0, y0, x1, y1) {
+    if ((rows = parent._squarify) && (rows.ratio === ratio)) {
+      var rows,
+          row,
+          nodes,
+          i,
+          j = -1,
+          n,
+          m = rows.length,
+          value = parent.value;
+
+      while (++j < m) {
+        row = rows[j], nodes = row.children;
+        for (i = row.value = 0, n = nodes.length; i < n; ++i) row.value += nodes[i].value;
+        if (row.dice) treemapDice(row, x0, y0, x1, y0 += (y1 - y0) * row.value / value);
+        else treemapSlice(row, x0, y0, x0 += (x1 - x0) * row.value / value, y1);
+        value -= row.value;
+      }
+    } else {
+      parent._squarify = rows = squarifyRatio(ratio, parent, x0, y0, x1, y1);
+      rows.ratio = ratio;
+    }
+  }
+
+  resquarify.ratio = function(x) {
+    return custom((x = +x) > 1 ? x : 1);
+  };
+
+  return resquarify;
+})(phi);
+
+function area$2(polygon) {
+  var i = -1,
+      n = polygon.length,
+      a,
+      b = polygon[n - 1],
+      area = 0;
+
+  while (++i < n) {
+    a = b;
+    b = polygon[i];
+    area += a[1] * b[0] - a[0] * b[1];
+  }
+
+  return area / 2;
+}
+
+function centroid$1(polygon) {
+  var i = -1,
+      n = polygon.length,
+      x = 0,
+      y = 0,
+      a,
+      b = polygon[n - 1],
+      c,
+      k = 0;
+
+  while (++i < n) {
+    a = b;
+    b = polygon[i];
+    k += c = a[0] * b[1] - b[0] * a[1];
+    x += (a[0] + b[0]) * c;
+    y += (a[1] + b[1]) * c;
+  }
+
+  return k *= 3, [x / k, y / k];
+}
+
+// Returns the 2D cross product of AB and AC vectors, i.e., the z-component of
+// the 3D cross product in a quadrant I Cartesian coordinate system (+x is
+// right, +y is up). Returns a positive value if ABC is counter-clockwise,
+// negative if clockwise, and zero if the points are collinear.
+function cross$1(a, b, c) {
+  return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
+}
+
+function lexicographicOrder(a, b) {
+  return a[0] - b[0] || a[1] - b[1];
+}
+
+// Computes the upper convex hull per the monotone chain algorithm.
+// Assumes points.length >= 3, is sorted by x, unique in y.
+// Returns an array of indices into points in left-to-right order.
+function computeUpperHullIndexes(points) {
+  var n = points.length,
+      indexes = [0, 1],
+      size = 2;
+
+  for (var i = 2; i < n; ++i) {
+    while (size > 1 && cross$1(points[indexes[size - 2]], points[indexes[size - 1]], points[i]) <= 0) --size;
+    indexes[size++] = i;
+  }
+
+  return indexes.slice(0, size); // remove popped points
+}
+
+function hull(points) {
+  if ((n = points.length) < 3) return null;
+
+  var i,
+      n,
+      sortedPoints = new Array(n),
+      flippedPoints = new Array(n);
+
+  for (i = 0; i < n; ++i) sortedPoints[i] = [+points[i][0], +points[i][1], i];
+  sortedPoints.sort(lexicographicOrder);
+  for (i = 0; i < n; ++i) flippedPoints[i] = [sortedPoints[i][0], -sortedPoints[i][1]];
+
+  var upperIndexes = computeUpperHullIndexes(sortedPoints),
+      lowerIndexes = computeUpperHullIndexes(flippedPoints);
+
+  // Construct the hull polygon, removing possible duplicate endpoints.
+  var skipLeft = lowerIndexes[0] === upperIndexes[0],
+      skipRight = lowerIndexes[lowerIndexes.length - 1] === upperIndexes[upperIndexes.length - 1],
+      hull = [];
+
+  // Add upper hull in right-to-l order.
+  // Then add lower hull in left-to-right order.
+  for (i = upperIndexes.length - 1; i >= 0; --i) hull.push(points[sortedPoints[upperIndexes[i]][2]]);
+  for (i = +skipLeft; i < lowerIndexes.length - skipRight; ++i) hull.push(points[sortedPoints[lowerIndexes[i]][2]]);
+
+  return hull;
+}
+
+function contains$2(polygon, point) {
+  var n = polygon.length,
+      p = polygon[n - 1],
+      x = point[0], y = point[1],
+      x0 = p[0], y0 = p[1],
+      x1, y1,
+      inside = false;
+
+  for (var i = 0; i < n; ++i) {
+    p = polygon[i], x1 = p[0], y1 = p[1];
+    if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside;
+    x0 = x1, y0 = y1;
+  }
+
+  return inside;
+}
+
+function length$2(polygon) {
+  var i = -1,
+      n = polygon.length,
+      b = polygon[n - 1],
+      xa,
+      ya,
+      xb = b[0],
+      yb = b[1],
+      perimeter = 0;
+
+  while (++i < n) {
+    xa = xb;
+    ya = yb;
+    b = polygon[i];
+    xb = b[0];
+    yb = b[1];
+    xa -= xb;
+    ya -= yb;
+    perimeter += Math.sqrt(xa * xa + ya * ya);
+  }
+
+  return perimeter;
+}
+
+function defaultSource$1() {
+  return Math.random();
+}
+
+var uniform = (function sourceRandomUniform(source) {
+  function randomUniform(min, max) {
+    min = min == null ? 0 : +min;
+    max = max == null ? 1 : +max;
+    if (arguments.length === 1) max = min, min = 0;
+    else max -= min;
+    return function() {
+      return source() * max + min;
+    };
+  }
+
+  randomUniform.source = sourceRandomUniform;
+
+  return randomUniform;
+})(defaultSource$1);
+
+var normal = (function sourceRandomNormal(source) {
+  function randomNormal(mu, sigma) {
+    var x, r;
+    mu = mu == null ? 0 : +mu;
+    sigma = sigma == null ? 1 : +sigma;
+    return function() {
+      var y;
+
+      // If available, use the second previously-generated uniform random.
+      if (x != null) y = x, x = null;
+
+      // Otherwise, generate a new x and y.
+      else do {
+        x = source() * 2 - 1;
+        y = source() * 2 - 1;
+        r = x * x + y * y;
+      } while (!r || r > 1);
+
+      return mu + sigma * y * Math.sqrt(-2 * Math.log(r) / r);
+    };
+  }
+
+  randomNormal.source = sourceRandomNormal;
+
+  return randomNormal;
+})(defaultSource$1);
+
+var logNormal = (function sourceRandomLogNormal(source) {
+  function randomLogNormal() {
+    var randomNormal = normal.source(source).apply(this, arguments);
+    return function() {
+      return Math.exp(randomNormal());
+    };
+  }
+
+  randomLogNormal.source = sourceRandomLogNormal;
+
+  return randomLogNormal;
+})(defaultSource$1);
+
+var irwinHall = (function sourceRandomIrwinHall(source) {
+  function randomIrwinHall(n) {
+    return function() {
+      for (var sum = 0, i = 0; i < n; ++i) sum += source();
+      return sum;
+    };
+  }
+
+  randomIrwinHall.source = sourceRandomIrwinHall;
+
+  return randomIrwinHall;
+})(defaultSource$1);
+
+var bates = (function sourceRandomBates(source) {
+  function randomBates(n) {
+    var randomIrwinHall = irwinHall.source(source)(n);
+    return function() {
+      return randomIrwinHall() / n;
+    };
+  }
+
+  randomBates.source = sourceRandomBates;
+
+  return randomBates;
+})(defaultSource$1);
+
+var exponential$1 = (function sourceRandomExponential(source) {
+  function randomExponential(lambda) {
+    return function() {
+      return -Math.log(1 - source()) / lambda;
+    };
+  }
+
+  randomExponential.source = sourceRandomExponential;
+
+  return randomExponential;
+})(defaultSource$1);
+
+function initRange(domain, range) {
+  switch (arguments.length) {
+    case 0: break;
+    case 1: this.range(domain); break;
+    default: this.range(range).domain(domain); break;
+  }
+  return this;
+}
+
+function initInterpolator(domain, interpolator) {
+  switch (arguments.length) {
+    case 0: break;
+    case 1: this.interpolator(domain); break;
+    default: this.interpolator(interpolator).domain(domain); break;
+  }
+  return this;
+}
+
+var array$3 = Array.prototype;
+
+var map$2 = array$3.map;
+var slice$5 = array$3.slice;
+
+var implicit = {name: "implicit"};
+
+function ordinal() {
+  var index = map$1(),
+      domain = [],
+      range = [],
+      unknown = implicit;
+
+  function scale(d) {
+    var key = d + "", i = index.get(key);
+    if (!i) {
+      if (unknown !== implicit) return unknown;
+      index.set(key, i = domain.push(d));
+    }
+    return range[(i - 1) % range.length];
+  }
+
+  scale.domain = function(_) {
+    if (!arguments.length) return domain.slice();
+    domain = [], index = map$1();
+    var i = -1, n = _.length, d, key;
+    while (++i < n) if (!index.has(key = (d = _[i]) + "")) index.set(key, domain.push(d));
+    return scale;
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (range = slice$5.call(_), scale) : range.slice();
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  scale.copy = function() {
+    return ordinal(domain, range).unknown(unknown);
+  };
+
+  initRange.apply(scale, arguments);
+
+  return scale;
+}
+
+function band() {
+  var scale = ordinal().unknown(undefined),
+      domain = scale.domain,
+      ordinalRange = scale.range,
+      range$$1 = [0, 1],
+      step,
+      bandwidth,
+      round = false,
+      paddingInner = 0,
+      paddingOuter = 0,
+      align = 0.5;
+
+  delete scale.unknown;
+
+  function rescale() {
+    var n = domain().length,
+        reverse = range$$1[1] < range$$1[0],
+        start = range$$1[reverse - 0],
+        stop = range$$1[1 - reverse];
+    step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2);
+    if (round) step = Math.floor(step);
+    start += (stop - start - step * (n - paddingInner)) * align;
+    bandwidth = step * (1 - paddingInner);
+    if (round) start = Math.round(start), bandwidth = Math.round(bandwidth);
+    var values = sequence(n).map(function(i) { return start + step * i; });
+    return ordinalRange(reverse ? values.reverse() : values);
+  }
+
+  scale.domain = function(_) {
+    return arguments.length ? (domain(_), rescale()) : domain();
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (range$$1 = [+_[0], +_[1]], rescale()) : range$$1.slice();
+  };
+
+  scale.rangeRound = function(_) {
+    return range$$1 = [+_[0], +_[1]], round = true, rescale();
+  };
+
+  scale.bandwidth = function() {
+    return bandwidth;
+  };
+
+  scale.step = function() {
+    return step;
+  };
+
+  scale.round = function(_) {
+    return arguments.length ? (round = !!_, rescale()) : round;
+  };
+
+  scale.padding = function(_) {
+    return arguments.length ? (paddingInner = Math.min(1, paddingOuter = +_), rescale()) : paddingInner;
+  };
+
+  scale.paddingInner = function(_) {
+    return arguments.length ? (paddingInner = Math.min(1, _), rescale()) : paddingInner;
+  };
+
+  scale.paddingOuter = function(_) {
+    return arguments.length ? (paddingOuter = +_, rescale()) : paddingOuter;
+  };
+
+  scale.align = function(_) {
+    return arguments.length ? (align = Math.max(0, Math.min(1, _)), rescale()) : align;
+  };
+
+  scale.copy = function() {
+    return band(domain(), range$$1)
+        .round(round)
+        .paddingInner(paddingInner)
+        .paddingOuter(paddingOuter)
+        .align(align);
+  };
+
+  return initRange.apply(rescale(), arguments);
+}
+
+function pointish(scale) {
+  var copy = scale.copy;
+
+  scale.padding = scale.paddingOuter;
+  delete scale.paddingInner;
+  delete scale.paddingOuter;
+
+  scale.copy = function() {
+    return pointish(copy());
+  };
+
+  return scale;
+}
+
+function point$1() {
+  return pointish(band.apply(null, arguments).paddingInner(1));
+}
+
+function constant$a(x) {
+  return function() {
+    return x;
+  };
+}
+
+function number$2(x) {
+  return +x;
+}
+
+var unit = [0, 1];
+
+function identity$6(x) {
+  return x;
+}
+
+function normalize(a, b) {
+  return (b -= (a = +a))
+      ? function(x) { return (x - a) / b; }
+      : constant$a(isNaN(b) ? NaN : 0.5);
+}
+
+function clamper(domain) {
+  var a = domain[0], b = domain[domain.length - 1], t;
+  if (a > b) t = a, a = b, b = t;
+  return function(x) { return Math.max(a, Math.min(b, x)); };
+}
+
+// normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].
+// interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b].
+function bimap(domain, range, interpolate$$1) {
+  var d0 = domain[0], d1 = domain[1], r0 = range[0], r1 = range[1];
+  if (d1 < d0) d0 = normalize(d1, d0), r0 = interpolate$$1(r1, r0);
+  else d0 = normalize(d0, d1), r0 = interpolate$$1(r0, r1);
+  return function(x) { return r0(d0(x)); };
+}
+
+function polymap(domain, range, interpolate$$1) {
+  var j = Math.min(domain.length, range.length) - 1,
+      d = new Array(j),
+      r = new Array(j),
+      i = -1;
+
+  // Reverse descending domains.
+  if (domain[j] < domain[0]) {
+    domain = domain.slice().reverse();
+    range = range.slice().reverse();
+  }
+
+  while (++i < j) {
+    d[i] = normalize(domain[i], domain[i + 1]);
+    r[i] = interpolate$$1(range[i], range[i + 1]);
+  }
+
+  return function(x) {
+    var i = bisectRight(domain, x, 1, j) - 1;
+    return r[i](d[i](x));
+  };
+}
+
+function copy(source, target) {
+  return target
+      .domain(source.domain())
+      .range(source.range())
+      .interpolate(source.interpolate())
+      .clamp(source.clamp())
+      .unknown(source.unknown());
+}
+
+function transformer$1() {
+  var domain = unit,
+      range = unit,
+      interpolate$$1 = interpolateValue,
+      transform,
+      untransform,
+      unknown,
+      clamp = identity$6,
+      piecewise$$1,
+      output,
+      input;
+
+  function rescale() {
+    piecewise$$1 = Math.min(domain.length, range.length) > 2 ? polymap : bimap;
+    output = input = null;
+    return scale;
+  }
+
+  function scale(x) {
+    return isNaN(x = +x) ? unknown : (output || (output = piecewise$$1(domain.map(transform), range, interpolate$$1)))(transform(clamp(x)));
+  }
+
+  scale.invert = function(y) {
+    return clamp(untransform((input || (input = piecewise$$1(range, domain.map(transform), interpolateNumber)))(y)));
+  };
+
+  scale.domain = function(_) {
+    return arguments.length ? (domain = map$2.call(_, number$2), clamp === identity$6 || (clamp = clamper(domain)), rescale()) : domain.slice();
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (range = slice$5.call(_), rescale()) : range.slice();
+  };
+
+  scale.rangeRound = function(_) {
+    return range = slice$5.call(_), interpolate$$1 = interpolateRound, rescale();
+  };
+
+  scale.clamp = function(_) {
+    return arguments.length ? (clamp = _ ? clamper(domain) : identity$6, scale) : clamp !== identity$6;
+  };
+
+  scale.interpolate = function(_) {
+    return arguments.length ? (interpolate$$1 = _, rescale()) : interpolate$$1;
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  return function(t, u) {
+    transform = t, untransform = u;
+    return rescale();
+  };
+}
+
+function continuous(transform, untransform) {
+  return transformer$1()(transform, untransform);
+}
+
+function tickFormat(start, stop, count, specifier) {
+  var step = tickStep(start, stop, count),
+      precision;
+  specifier = formatSpecifier(specifier == null ? ",f" : specifier);
+  switch (specifier.type) {
+    case "s": {
+      var value = Math.max(Math.abs(start), Math.abs(stop));
+      if (specifier.precision == null && !isNaN(precision = precisionPrefix(step, value))) specifier.precision = precision;
+      return exports.formatPrefix(specifier, value);
+    }
+    case "":
+    case "e":
+    case "g":
+    case "p":
+    case "r": {
+      if (specifier.precision == null && !isNaN(precision = precisionRound(step, Math.max(Math.abs(start), Math.abs(stop))))) specifier.precision = precision - (specifier.type === "e");
+      break;
+    }
+    case "f":
+    case "%": {
+      if (specifier.precision == null && !isNaN(precision = precisionFixed(step))) specifier.precision = precision - (specifier.type === "%") * 2;
+      break;
+    }
+  }
+  return exports.format(specifier);
+}
+
+function linearish(scale) {
+  var domain = scale.domain;
+
+  scale.ticks = function(count) {
+    var d = domain();
+    return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
+  };
+
+  scale.tickFormat = function(count, specifier) {
+    var d = domain();
+    return tickFormat(d[0], d[d.length - 1], count == null ? 10 : count, specifier);
+  };
+
+  scale.nice = function(count) {
+    if (count == null) count = 10;
+
+    var d = domain(),
+        i0 = 0,
+        i1 = d.length - 1,
+        start = d[i0],
+        stop = d[i1],
+        step;
+
+    if (stop < start) {
+      step = start, start = stop, stop = step;
+      step = i0, i0 = i1, i1 = step;
+    }
+
+    step = tickIncrement(start, stop, count);
+
+    if (step > 0) {
+      start = Math.floor(start / step) * step;
+      stop = Math.ceil(stop / step) * step;
+      step = tickIncrement(start, stop, count);
+    } else if (step < 0) {
+      start = Math.ceil(start * step) / step;
+      stop = Math.floor(stop * step) / step;
+      step = tickIncrement(start, stop, count);
+    }
+
+    if (step > 0) {
+      d[i0] = Math.floor(start / step) * step;
+      d[i1] = Math.ceil(stop / step) * step;
+      domain(d);
+    } else if (step < 0) {
+      d[i0] = Math.ceil(start * step) / step;
+      d[i1] = Math.floor(stop * step) / step;
+      domain(d);
+    }
+
+    return scale;
+  };
+
+  return scale;
+}
+
+function linear$2() {
+  var scale = continuous(identity$6, identity$6);
+
+  scale.copy = function() {
+    return copy(scale, linear$2());
+  };
+
+  initRange.apply(scale, arguments);
+
+  return linearish(scale);
+}
+
+function identity$7(domain) {
+  var unknown;
+
+  function scale(x) {
+    return isNaN(x = +x) ? unknown : x;
+  }
+
+  scale.invert = scale;
+
+  scale.domain = scale.range = function(_) {
+    return arguments.length ? (domain = map$2.call(_, number$2), scale) : domain.slice();
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  scale.copy = function() {
+    return identity$7(domain).unknown(unknown);
+  };
+
+  domain = arguments.length ? map$2.call(domain, number$2) : [0, 1];
+
+  return linearish(scale);
+}
+
+function nice(domain, interval) {
+  domain = domain.slice();
+
+  var i0 = 0,
+      i1 = domain.length - 1,
+      x0 = domain[i0],
+      x1 = domain[i1],
+      t;
+
+  if (x1 < x0) {
+    t = i0, i0 = i1, i1 = t;
+    t = x0, x0 = x1, x1 = t;
+  }
+
+  domain[i0] = interval.floor(x0);
+  domain[i1] = interval.ceil(x1);
+  return domain;
+}
+
+function transformLog(x) {
+  return Math.log(x);
+}
+
+function transformExp(x) {
+  return Math.exp(x);
+}
+
+function transformLogn(x) {
+  return -Math.log(-x);
+}
+
+function transformExpn(x) {
+  return -Math.exp(-x);
+}
+
+function pow10(x) {
+  return isFinite(x) ? +("1e" + x) : x < 0 ? 0 : x;
+}
+
+function powp(base) {
+  return base === 10 ? pow10
+      : base === Math.E ? Math.exp
+      : function(x) { return Math.pow(base, x); };
+}
+
+function logp(base) {
+  return base === Math.E ? Math.log
+      : base === 10 && Math.log10
+      || base === 2 && Math.log2
+      || (base = Math.log(base), function(x) { return Math.log(x) / base; });
+}
+
+function reflect(f) {
+  return function(x) {
+    return -f(-x);
+  };
+}
+
+function loggish(transform) {
+  var scale = transform(transformLog, transformExp),
+      domain = scale.domain,
+      base = 10,
+      logs,
+      pows;
+
+  function rescale() {
+    logs = logp(base), pows = powp(base);
+    if (domain()[0] < 0) {
+      logs = reflect(logs), pows = reflect(pows);
+      transform(transformLogn, transformExpn);
+    } else {
+      transform(transformLog, transformExp);
+    }
+    return scale;
+  }
+
+  scale.base = function(_) {
+    return arguments.length ? (base = +_, rescale()) : base;
+  };
+
+  scale.domain = function(_) {
+    return arguments.length ? (domain(_), rescale()) : domain();
+  };
+
+  scale.ticks = function(count) {
+    var d = domain(),
+        u = d[0],
+        v = d[d.length - 1],
+        r;
+
+    if (r = v < u) i = u, u = v, v = i;
+
+    var i = logs(u),
+        j = logs(v),
+        p,
+        k,
+        t,
+        n = count == null ? 10 : +count,
+        z = [];
+
+    if (!(base % 1) && j - i < n) {
+      i = Math.round(i) - 1, j = Math.round(j) + 1;
+      if (u > 0) for (; i < j; ++i) {
+        for (k = 1, p = pows(i); k < base; ++k) {
+          t = p * k;
+          if (t < u) continue;
+          if (t > v) break;
+          z.push(t);
+        }
+      } else for (; i < j; ++i) {
+        for (k = base - 1, p = pows(i); k >= 1; --k) {
+          t = p * k;
+          if (t < u) continue;
+          if (t > v) break;
+          z.push(t);
+        }
+      }
+    } else {
+      z = ticks(i, j, Math.min(j - i, n)).map(pows);
+    }
+
+    return r ? z.reverse() : z;
+  };
+
+  scale.tickFormat = function(count, specifier) {
+    if (specifier == null) specifier = base === 10 ? ".0e" : ",";
+    if (typeof specifier !== "function") specifier = exports.format(specifier);
+    if (count === Infinity) return specifier;
+    if (count == null) count = 10;
+    var k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?
+    return function(d) {
+      var i = d / pows(Math.round(logs(d)));
+      if (i * base < base - 0.5) i *= base;
+      return i <= k ? specifier(d) : "";
+    };
+  };
+
+  scale.nice = function() {
+    return domain(nice(domain(), {
+      floor: function(x) { return pows(Math.floor(logs(x))); },
+      ceil: function(x) { return pows(Math.ceil(logs(x))); }
+    }));
+  };
+
+  return scale;
+}
+
+function log$1() {
+  var scale = loggish(transformer$1()).domain([1, 10]);
+
+  scale.copy = function() {
+    return copy(scale, log$1()).base(scale.base());
+  };
+
+  initRange.apply(scale, arguments);
+
+  return scale;
+}
+
+function transformSymlog(c) {
+  return function(x) {
+    return Math.sign(x) * Math.log1p(Math.abs(x / c));
+  };
+}
+
+function transformSymexp(c) {
+  return function(x) {
+    return Math.sign(x) * Math.expm1(Math.abs(x)) * c;
+  };
+}
+
+function symlogish(transform) {
+  var c = 1, scale = transform(transformSymlog(c), transformSymexp(c));
+
+  scale.constant = function(_) {
+    return arguments.length ? transform(transformSymlog(c = +_), transformSymexp(c)) : c;
+  };
+
+  return linearish(scale);
+}
+
+function symlog() {
+  var scale = symlogish(transformer$1());
+
+  scale.copy = function() {
+    return copy(scale, symlog()).constant(scale.constant());
+  };
+
+  return initRange.apply(scale, arguments);
+}
+
+function transformPow(exponent) {
+  return function(x) {
+    return x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent);
+  };
+}
+
+function transformSqrt(x) {
+  return x < 0 ? -Math.sqrt(-x) : Math.sqrt(x);
+}
+
+function transformSquare(x) {
+  return x < 0 ? -x * x : x * x;
+}
+
+function powish(transform) {
+  var scale = transform(identity$6, identity$6),
+      exponent = 1;
+
+  function rescale() {
+    return exponent === 1 ? transform(identity$6, identity$6)
+        : exponent === 0.5 ? transform(transformSqrt, transformSquare)
+        : transform(transformPow(exponent), transformPow(1 / exponent));
+  }
+
+  scale.exponent = function(_) {
+    return arguments.length ? (exponent = +_, rescale()) : exponent;
+  };
+
+  return linearish(scale);
+}
+
+function pow$1() {
+  var scale = powish(transformer$1());
+
+  scale.copy = function() {
+    return copy(scale, pow$1()).exponent(scale.exponent());
+  };
+
+  initRange.apply(scale, arguments);
+
+  return scale;
+}
+
+function sqrt$1() {
+  return pow$1.apply(null, arguments).exponent(0.5);
+}
+
+function quantile$$1() {
+  var domain = [],
+      range = [],
+      thresholds = [],
+      unknown;
+
+  function rescale() {
+    var i = 0, n = Math.max(1, range.length);
+    thresholds = new Array(n - 1);
+    while (++i < n) thresholds[i - 1] = threshold(domain, i / n);
+    return scale;
+  }
+
+  function scale(x) {
+    return isNaN(x = +x) ? unknown : range[bisectRight(thresholds, x)];
+  }
+
+  scale.invertExtent = function(y) {
+    var i = range.indexOf(y);
+    return i < 0 ? [NaN, NaN] : [
+      i > 0 ? thresholds[i - 1] : domain[0],
+      i < thresholds.length ? thresholds[i] : domain[domain.length - 1]
+    ];
+  };
+
+  scale.domain = function(_) {
+    if (!arguments.length) return domain.slice();
+    domain = [];
+    for (var i = 0, n = _.length, d; i < n; ++i) if (d = _[i], d != null && !isNaN(d = +d)) domain.push(d);
+    domain.sort(ascending);
+    return rescale();
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (range = slice$5.call(_), rescale()) : range.slice();
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  scale.quantiles = function() {
+    return thresholds.slice();
+  };
+
+  scale.copy = function() {
+    return quantile$$1()
+        .domain(domain)
+        .range(range)
+        .unknown(unknown);
+  };
+
+  return initRange.apply(scale, arguments);
+}
+
+function quantize$1() {
+  var x0 = 0,
+      x1 = 1,
+      n = 1,
+      domain = [0.5],
+      range = [0, 1],
+      unknown;
+
+  function scale(x) {
+    return x <= x ? range[bisectRight(domain, x, 0, n)] : unknown;
+  }
+
+  function rescale() {
+    var i = -1;
+    domain = new Array(n);
+    while (++i < n) domain[i] = ((i + 1) * x1 - (i - n) * x0) / (n + 1);
+    return scale;
+  }
+
+  scale.domain = function(_) {
+    return arguments.length ? (x0 = +_[0], x1 = +_[1], rescale()) : [x0, x1];
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (n = (range = slice$5.call(_)).length - 1, rescale()) : range.slice();
+  };
+
+  scale.invertExtent = function(y) {
+    var i = range.indexOf(y);
+    return i < 0 ? [NaN, NaN]
+        : i < 1 ? [x0, domain[0]]
+        : i >= n ? [domain[n - 1], x1]
+        : [domain[i - 1], domain[i]];
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : scale;
+  };
+
+  scale.thresholds = function() {
+    return domain.slice();
+  };
+
+  scale.copy = function() {
+    return quantize$1()
+        .domain([x0, x1])
+        .range(range)
+        .unknown(unknown);
+  };
+
+  return initRange.apply(linearish(scale), arguments);
+}
+
+function threshold$1() {
+  var domain = [0.5],
+      range = [0, 1],
+      unknown,
+      n = 1;
+
+  function scale(x) {
+    return x <= x ? range[bisectRight(domain, x, 0, n)] : unknown;
+  }
+
+  scale.domain = function(_) {
+    return arguments.length ? (domain = slice$5.call(_), n = Math.min(domain.length, range.length - 1), scale) : domain.slice();
+  };
+
+  scale.range = function(_) {
+    return arguments.length ? (range = slice$5.call(_), n = Math.min(domain.length, range.length - 1), scale) : range.slice();
+  };
+
+  scale.invertExtent = function(y) {
+    var i = range.indexOf(y);
+    return [domain[i - 1], domain[i]];
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  scale.copy = function() {
+    return threshold$1()
+        .domain(domain)
+        .range(range)
+        .unknown(unknown);
+  };
+
+  return initRange.apply(scale, arguments);
+}
+
+var t0$1 = new Date,
+    t1$1 = new Date;
+
+function newInterval(floori, offseti, count, field) {
+
+  function interval(date) {
+    return floori(date = new Date(+date)), date;
+  }
+
+  interval.floor = interval;
+
+  interval.ceil = function(date) {
+    return floori(date = new Date(date - 1)), offseti(date, 1), floori(date), date;
+  };
+
+  interval.round = function(date) {
+    var d0 = interval(date),
+        d1 = interval.ceil(date);
+    return date - d0 < d1 - date ? d0 : d1;
+  };
+
+  interval.offset = function(date, step) {
+    return offseti(date = new Date(+date), step == null ? 1 : Math.floor(step)), date;
+  };
+
+  interval.range = function(start, stop, step) {
+    var range = [], previous;
+    start = interval.ceil(start);
+    step = step == null ? 1 : Math.floor(step);
+    if (!(start < stop) || !(step > 0)) return range; // also handles Invalid Date
+    do range.push(previous = new Date(+start)), offseti(start, step), floori(start);
+    while (previous < start && start < stop);
+    return range;
+  };
+
+  interval.filter = function(test) {
+    return newInterval(function(date) {
+      if (date >= date) while (floori(date), !test(date)) date.setTime(date - 1);
+    }, function(date, step) {
+      if (date >= date) {
+        if (step < 0) while (++step <= 0) {
+          while (offseti(date, -1), !test(date)) {} // eslint-disable-line no-empty
+        } else while (--step >= 0) {
+          while (offseti(date, +1), !test(date)) {} // eslint-disable-line no-empty
+        }
+      }
+    });
+  };
+
+  if (count) {
+    interval.count = function(start, end) {
+      t0$1.setTime(+start), t1$1.setTime(+end);
+      floori(t0$1), floori(t1$1);
+      return Math.floor(count(t0$1, t1$1));
+    };
+
+    interval.every = function(step) {
+      step = Math.floor(step);
+      return !isFinite(step) || !(step > 0) ? null
+          : !(step > 1) ? interval
+          : interval.filter(field
+              ? function(d) { return field(d) % step === 0; }
+              : function(d) { return interval.count(0, d) % step === 0; });
+    };
+  }
+
+  return interval;
+}
+
+var millisecond = newInterval(function() {
+  // noop
+}, function(date, step) {
+  date.setTime(+date + step);
+}, function(start, end) {
+  return end - start;
+});
+
+// An optimized implementation for this simple case.
+millisecond.every = function(k) {
+  k = Math.floor(k);
+  if (!isFinite(k) || !(k > 0)) return null;
+  if (!(k > 1)) return millisecond;
+  return newInterval(function(date) {
+    date.setTime(Math.floor(date / k) * k);
+  }, function(date, step) {
+    date.setTime(+date + step * k);
+  }, function(start, end) {
+    return (end - start) / k;
+  });
+};
+var milliseconds = millisecond.range;
+
+var durationSecond = 1e3;
+var durationMinute = 6e4;
+var durationHour = 36e5;
+var durationDay = 864e5;
+var durationWeek = 6048e5;
+
+var second = newInterval(function(date) {
+  date.setTime(date - date.getMilliseconds());
+}, function(date, step) {
+  date.setTime(+date + step * durationSecond);
+}, function(start, end) {
+  return (end - start) / durationSecond;
+}, function(date) {
+  return date.getUTCSeconds();
+});
+var seconds = second.range;
+
+var minute = newInterval(function(date) {
+  date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond);
+}, function(date, step) {
+  date.setTime(+date + step * durationMinute);
+}, function(start, end) {
+  return (end - start) / durationMinute;
+}, function(date) {
+  return date.getMinutes();
+});
+var minutes = minute.range;
+
+var hour = newInterval(function(date) {
+  date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute);
+}, function(date, step) {
+  date.setTime(+date + step * durationHour);
+}, function(start, end) {
+  return (end - start) / durationHour;
+}, function(date) {
+  return date.getHours();
+});
+var hours = hour.range;
+
+var day = newInterval(function(date) {
+  date.setHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setDate(date.getDate() + step);
+}, function(start, end) {
+  return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationDay;
+}, function(date) {
+  return date.getDate() - 1;
+});
+var days = day.range;
+
+function weekday(i) {
+  return newInterval(function(date) {
+    date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);
+    date.setHours(0, 0, 0, 0);
+  }, function(date, step) {
+    date.setDate(date.getDate() + step * 7);
+  }, function(start, end) {
+    return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek;
+  });
+}
+
+var sunday = weekday(0);
+var monday = weekday(1);
+var tuesday = weekday(2);
+var wednesday = weekday(3);
+var thursday = weekday(4);
+var friday = weekday(5);
+var saturday = weekday(6);
+
+var sundays = sunday.range;
+var mondays = monday.range;
+var tuesdays = tuesday.range;
+var wednesdays = wednesday.range;
+var thursdays = thursday.range;
+var fridays = friday.range;
+var saturdays = saturday.range;
+
+var month = newInterval(function(date) {
+  date.setDate(1);
+  date.setHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setMonth(date.getMonth() + step);
+}, function(start, end) {
+  return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;
+}, function(date) {
+  return date.getMonth();
+});
+var months = month.range;
+
+var year = newInterval(function(date) {
+  date.setMonth(0, 1);
+  date.setHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setFullYear(date.getFullYear() + step);
+}, function(start, end) {
+  return end.getFullYear() - start.getFullYear();
+}, function(date) {
+  return date.getFullYear();
+});
+
+// An optimized implementation for this simple case.
+year.every = function(k) {
+  return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : newInterval(function(date) {
+    date.setFullYear(Math.floor(date.getFullYear() / k) * k);
+    date.setMonth(0, 1);
+    date.setHours(0, 0, 0, 0);
+  }, function(date, step) {
+    date.setFullYear(date.getFullYear() + step * k);
+  });
+};
+var years = year.range;
+
+var utcMinute = newInterval(function(date) {
+  date.setUTCSeconds(0, 0);
+}, function(date, step) {
+  date.setTime(+date + step * durationMinute);
+}, function(start, end) {
+  return (end - start) / durationMinute;
+}, function(date) {
+  return date.getUTCMinutes();
+});
+var utcMinutes = utcMinute.range;
+
+var utcHour = newInterval(function(date) {
+  date.setUTCMinutes(0, 0, 0);
+}, function(date, step) {
+  date.setTime(+date + step * durationHour);
+}, function(start, end) {
+  return (end - start) / durationHour;
+}, function(date) {
+  return date.getUTCHours();
+});
+var utcHours = utcHour.range;
+
+var utcDay = newInterval(function(date) {
+  date.setUTCHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setUTCDate(date.getUTCDate() + step);
+}, function(start, end) {
+  return (end - start) / durationDay;
+}, function(date) {
+  return date.getUTCDate() - 1;
+});
+var utcDays = utcDay.range;
+
+function utcWeekday(i) {
+  return newInterval(function(date) {
+    date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7);
+    date.setUTCHours(0, 0, 0, 0);
+  }, function(date, step) {
+    date.setUTCDate(date.getUTCDate() + step * 7);
+  }, function(start, end) {
+    return (end - start) / durationWeek;
+  });
+}
+
+var utcSunday = utcWeekday(0);
+var utcMonday = utcWeekday(1);
+var utcTuesday = utcWeekday(2);
+var utcWednesday = utcWeekday(3);
+var utcThursday = utcWeekday(4);
+var utcFriday = utcWeekday(5);
+var utcSaturday = utcWeekday(6);
+
+var utcSundays = utcSunday.range;
+var utcMondays = utcMonday.range;
+var utcTuesdays = utcTuesday.range;
+var utcWednesdays = utcWednesday.range;
+var utcThursdays = utcThursday.range;
+var utcFridays = utcFriday.range;
+var utcSaturdays = utcSaturday.range;
+
+var utcMonth = newInterval(function(date) {
+  date.setUTCDate(1);
+  date.setUTCHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setUTCMonth(date.getUTCMonth() + step);
+}, function(start, end) {
+  return end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12;
+}, function(date) {
+  return date.getUTCMonth();
+});
+var utcMonths = utcMonth.range;
+
+var utcYear = newInterval(function(date) {
+  date.setUTCMonth(0, 1);
+  date.setUTCHours(0, 0, 0, 0);
+}, function(date, step) {
+  date.setUTCFullYear(date.getUTCFullYear() + step);
+}, function(start, end) {
+  return end.getUTCFullYear() - start.getUTCFullYear();
+}, function(date) {
+  return date.getUTCFullYear();
+});
+
+// An optimized implementation for this simple case.
+utcYear.every = function(k) {
+  return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : newInterval(function(date) {
+    date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k);
+    date.setUTCMonth(0, 1);
+    date.setUTCHours(0, 0, 0, 0);
+  }, function(date, step) {
+    date.setUTCFullYear(date.getUTCFullYear() + step * k);
+  });
+};
+var utcYears = utcYear.range;
+
+function localDate(d) {
+  if (0 <= d.y && d.y < 100) {
+    var date = new Date(-1, d.m, d.d, d.H, d.M, d.S, d.L);
+    date.setFullYear(d.y);
+    return date;
+  }
+  return new Date(d.y, d.m, d.d, d.H, d.M, d.S, d.L);
+}
+
+function utcDate(d) {
+  if (0 <= d.y && d.y < 100) {
+    var date = new Date(Date.UTC(-1, d.m, d.d, d.H, d.M, d.S, d.L));
+    date.setUTCFullYear(d.y);
+    return date;
+  }
+  return new Date(Date.UTC(d.y, d.m, d.d, d.H, d.M, d.S, d.L));
+}
+
+function newYear(y) {
+  return {y: y, m: 0, d: 1, H: 0, M: 0, S: 0, L: 0};
+}
+
+function formatLocale$1(locale) {
+  var locale_dateTime = locale.dateTime,
+      locale_date = locale.date,
+      locale_time = locale.time,
+      locale_periods = locale.periods,
+      locale_weekdays = locale.days,
+      locale_shortWeekdays = locale.shortDays,
+      locale_months = locale.months,
+      locale_shortMonths = locale.shortMonths;
+
+  var periodRe = formatRe(locale_periods),
+      periodLookup = formatLookup(locale_periods),
+      weekdayRe = formatRe(locale_weekdays),
+      weekdayLookup = formatLookup(locale_weekdays),
+      shortWeekdayRe = formatRe(locale_shortWeekdays),
+      shortWeekdayLookup = formatLookup(locale_shortWeekdays),
+      monthRe = formatRe(locale_months),
+      monthLookup = formatLookup(locale_months),
+      shortMonthRe = formatRe(locale_shortMonths),
+      shortMonthLookup = formatLookup(locale_shortMonths);
+
+  var formats = {
+    "a": formatShortWeekday,
+    "A": formatWeekday,
+    "b": formatShortMonth,
+    "B": formatMonth,
+    "c": null,
+    "d": formatDayOfMonth,
+    "e": formatDayOfMonth,
+    "f": formatMicroseconds,
+    "H": formatHour24,
+    "I": formatHour12,
+    "j": formatDayOfYear,
+    "L": formatMilliseconds,
+    "m": formatMonthNumber,
+    "M": formatMinutes,
+    "p": formatPeriod,
+    "Q": formatUnixTimestamp,
+    "s": formatUnixTimestampSeconds,
+    "S": formatSeconds,
+    "u": formatWeekdayNumberMonday,
+    "U": formatWeekNumberSunday,
+    "V": formatWeekNumberISO,
+    "w": formatWeekdayNumberSunday,
+    "W": formatWeekNumberMonday,
+    "x": null,
+    "X": null,
+    "y": formatYear$1,
+    "Y": formatFullYear,
+    "Z": formatZone,
+    "%": formatLiteralPercent
+  };
+
+  var utcFormats = {
+    "a": formatUTCShortWeekday,
+    "A": formatUTCWeekday,
+    "b": formatUTCShortMonth,
+    "B": formatUTCMonth,
+    "c": null,
+    "d": formatUTCDayOfMonth,
+    "e": formatUTCDayOfMonth,
+    "f": formatUTCMicroseconds,
+    "H": formatUTCHour24,
+    "I": formatUTCHour12,
+    "j": formatUTCDayOfYear,
+    "L": formatUTCMilliseconds,
+    "m": formatUTCMonthNumber,
+    "M": formatUTCMinutes,
+    "p": formatUTCPeriod,
+    "Q": formatUnixTimestamp,
+    "s": formatUnixTimestampSeconds,
+    "S": formatUTCSeconds,
+    "u": formatUTCWeekdayNumberMonday,
+    "U": formatUTCWeekNumberSunday,
+    "V": formatUTCWeekNumberISO,
+    "w": formatUTCWeekdayNumberSunday,
+    "W": formatUTCWeekNumberMonday,
+    "x": null,
+    "X": null,
+    "y": formatUTCYear,
+    "Y": formatUTCFullYear,
+    "Z": formatUTCZone,
+    "%": formatLiteralPercent
+  };
+
+  var parses = {
+    "a": parseShortWeekday,
+    "A": parseWeekday,
+    "b": parseShortMonth,
+    "B": parseMonth,
+    "c": parseLocaleDateTime,
+    "d": parseDayOfMonth,
+    "e": parseDayOfMonth,
+    "f": parseMicroseconds,
+    "H": parseHour24,
+    "I": parseHour24,
+    "j": parseDayOfYear,
+    "L": parseMilliseconds,
+    "m": parseMonthNumber,
+    "M": parseMinutes,
+    "p": parsePeriod,
+    "Q": parseUnixTimestamp,
+    "s": parseUnixTimestampSeconds,
+    "S": parseSeconds,
+    "u": parseWeekdayNumberMonday,
+    "U": parseWeekNumberSunday,
+    "V": parseWeekNumberISO,
+    "w": parseWeekdayNumberSunday,
+    "W": parseWeekNumberMonday,
+    "x": parseLocaleDate,
+    "X": parseLocaleTime,
+    "y": parseYear,
+    "Y": parseFullYear,
+    "Z": parseZone,
+    "%": parseLiteralPercent
+  };
+
+  // These recursive directive definitions must be deferred.
+  formats.x = newFormat(locale_date, formats);
+  formats.X = newFormat(locale_time, formats);
+  formats.c = newFormat(locale_dateTime, formats);
+  utcFormats.x = newFormat(locale_date, utcFormats);
+  utcFormats.X = newFormat(locale_time, utcFormats);
+  utcFormats.c = newFormat(locale_dateTime, utcFormats);
+
+  function newFormat(specifier, formats) {
+    return function(date) {
+      var string = [],
+          i = -1,
+          j = 0,
+          n = specifier.length,
+          c,
+          pad,
+          format;
+
+      if (!(date instanceof Date)) date = new Date(+date);
+
+      while (++i < n) {
+        if (specifier.charCodeAt(i) === 37) {
+          string.push(specifier.slice(j, i));
+          if ((pad = pads[c = specifier.charAt(++i)]) != null) c = specifier.charAt(++i);
+          else pad = c === "e" ? " " : "0";
+          if (format = formats[c]) c = format(date, pad);
+          string.push(c);
+          j = i + 1;
+        }
+      }
+
+      string.push(specifier.slice(j, i));
+      return string.join("");
+    };
+  }
+
+  function newParse(specifier, newDate) {
+    return function(string) {
+      var d = newYear(1900),
+          i = parseSpecifier(d, specifier, string += "", 0),
+          week, day$$1;
+      if (i != string.length) return null;
+
+      // If a UNIX timestamp is specified, return it.
+      if ("Q" in d) return new Date(d.Q);
+
+      // The am-pm flag is 0 for AM, and 1 for PM.
+      if ("p" in d) d.H = d.H % 12 + d.p * 12;
+
+      // Convert day-of-week and week-of-year to day-of-year.
+      if ("V" in d) {
+        if (d.V < 1 || d.V > 53) return null;
+        if (!("w" in d)) d.w = 1;
+        if ("Z" in d) {
+          week = utcDate(newYear(d.y)), day$$1 = week.getUTCDay();
+          week = day$$1 > 4 || day$$1 === 0 ? utcMonday.ceil(week) : utcMonday(week);
+          week = utcDay.offset(week, (d.V - 1) * 7);
+          d.y = week.getUTCFullYear();
+          d.m = week.getUTCMonth();
+          d.d = week.getUTCDate() + (d.w + 6) % 7;
+        } else {
+          week = newDate(newYear(d.y)), day$$1 = week.getDay();
+          week = day$$1 > 4 || day$$1 === 0 ? monday.ceil(week) : monday(week);
+          week = day.offset(week, (d.V - 1) * 7);
+          d.y = week.getFullYear();
+          d.m = week.getMonth();
+          d.d = week.getDate() + (d.w + 6) % 7;
+        }
+      } else if ("W" in d || "U" in d) {
+        if (!("w" in d)) d.w = "u" in d ? d.u % 7 : "W" in d ? 1 : 0;
+        day$$1 = "Z" in d ? utcDate(newYear(d.y)).getUTCDay() : newDate(newYear(d.y)).getDay();
+        d.m = 0;
+        d.d = "W" in d ? (d.w + 6) % 7 + d.W * 7 - (day$$1 + 5) % 7 : d.w + d.U * 7 - (day$$1 + 6) % 7;
+      }
+
+      // If a time zone is specified, all fields are interpreted as UTC and then
+      // offset according to the specified time zone.
+      if ("Z" in d) {
+        d.H += d.Z / 100 | 0;
+        d.M += d.Z % 100;
+        return utcDate(d);
+      }
+
+      // Otherwise, all fields are in local time.
+      return newDate(d);
+    };
+  }
+
+  function parseSpecifier(d, specifier, string, j) {
+    var i = 0,
+        n = specifier.length,
+        m = string.length,
+        c,
+        parse;
+
+    while (i < n) {
+      if (j >= m) return -1;
+      c = specifier.charCodeAt(i++);
+      if (c === 37) {
+        c = specifier.charAt(i++);
+        parse = parses[c in pads ? specifier.charAt(i++) : c];
+        if (!parse || ((j = parse(d, string, j)) < 0)) return -1;
+      } else if (c != string.charCodeAt(j++)) {
+        return -1;
+      }
+    }
+
+    return j;
+  }
+
+  function parsePeriod(d, string, i) {
+    var n = periodRe.exec(string.slice(i));
+    return n ? (d.p = periodLookup[n[0].toLowerCase()], i + n[0].length) : -1;
+  }
+
+  function parseShortWeekday(d, string, i) {
+    var n = shortWeekdayRe.exec(string.slice(i));
+    return n ? (d.w = shortWeekdayLookup[n[0].toLowerCase()], i + n[0].length) : -1;
+  }
+
+  function parseWeekday(d, string, i) {
+    var n = weekdayRe.exec(string.slice(i));
+    return n ? (d.w = weekdayLookup[n[0].toLowerCase()], i + n[0].length) : -1;
+  }
+
+  function parseShortMonth(d, string, i) {
+    var n = shortMonthRe.exec(string.slice(i));
+    return n ? (d.m = shortMonthLookup[n[0].toLowerCase()], i + n[0].length) : -1;
+  }
+
+  function parseMonth(d, string, i) {
+    var n = monthRe.exec(string.slice(i));
+    return n ? (d.m = monthLookup[n[0].toLowerCase()], i + n[0].length) : -1;
+  }
+
+  function parseLocaleDateTime(d, string, i) {
+    return parseSpecifier(d, locale_dateTime, string, i);
+  }
+
+  function parseLocaleDate(d, string, i) {
+    return parseSpecifier(d, locale_date, string, i);
+  }
+
+  function parseLocaleTime(d, string, i) {
+    return parseSpecifier(d, locale_time, string, i);
+  }
+
+  function formatShortWeekday(d) {
+    return locale_shortWeekdays[d.getDay()];
+  }
+
+  function formatWeekday(d) {
+    return locale_weekdays[d.getDay()];
+  }
+
+  function formatShortMonth(d) {
+    return locale_shortMonths[d.getMonth()];
+  }
+
+  function formatMonth(d) {
+    return locale_months[d.getMonth()];
+  }
+
+  function formatPeriod(d) {
+    return locale_periods[+(d.getHours() >= 12)];
+  }
+
+  function formatUTCShortWeekday(d) {
+    return locale_shortWeekdays[d.getUTCDay()];
+  }
+
+  function formatUTCWeekday(d) {
+    return locale_weekdays[d.getUTCDay()];
+  }
+
+  function formatUTCShortMonth(d) {
+    return locale_shortMonths[d.getUTCMonth()];
+  }
+
+  function formatUTCMonth(d) {
+    return locale_months[d.getUTCMonth()];
+  }
+
+  function formatUTCPeriod(d) {
+    return locale_periods[+(d.getUTCHours() >= 12)];
+  }
+
+  return {
+    format: function(specifier) {
+      var f = newFormat(specifier += "", formats);
+      f.toString = function() { return specifier; };
+      return f;
+    },
+    parse: function(specifier) {
+      var p = newParse(specifier += "", localDate);
+      p.toString = function() { return specifier; };
+      return p;
+    },
+    utcFormat: function(specifier) {
+      var f = newFormat(specifier += "", utcFormats);
+      f.toString = function() { return specifier; };
+      return f;
+    },
+    utcParse: function(specifier) {
+      var p = newParse(specifier, utcDate);
+      p.toString = function() { return specifier; };
+      return p;
+    }
+  };
+}
+
+var pads = {"-": "", "_": " ", "0": "0"},
+    numberRe = /^\s*\d+/, // note: ignores next directive
+    percentRe = /^%/,
+    requoteRe = /[\\^$*+?|[\]().{}]/g;
+
+function pad$1(value, fill, width) {
+  var sign = value < 0 ? "-" : "",
+      string = (sign ? -value : value) + "",
+      length = string.length;
+  return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);
+}
+
+function requote(s) {
+  return s.replace(requoteRe, "\\$&");
+}
+
+function formatRe(names) {
+  return new RegExp("^(?:" + names.map(requote).join("|") + ")", "i");
+}
+
+function formatLookup(names) {
+  var map = {}, i = -1, n = names.length;
+  while (++i < n) map[names[i].toLowerCase()] = i;
+  return map;
+}
+
+function parseWeekdayNumberSunday(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 1));
+  return n ? (d.w = +n[0], i + n[0].length) : -1;
+}
+
+function parseWeekdayNumberMonday(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 1));
+  return n ? (d.u = +n[0], i + n[0].length) : -1;
+}
+
+function parseWeekNumberSunday(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.U = +n[0], i + n[0].length) : -1;
+}
+
+function parseWeekNumberISO(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.V = +n[0], i + n[0].length) : -1;
+}
+
+function parseWeekNumberMonday(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.W = +n[0], i + n[0].length) : -1;
+}
+
+function parseFullYear(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 4));
+  return n ? (d.y = +n[0], i + n[0].length) : -1;
+}
+
+function parseYear(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.y = +n[0] + (+n[0] > 68 ? 1900 : 2000), i + n[0].length) : -1;
+}
+
+function parseZone(d, string, i) {
+  var n = /^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(string.slice(i, i + 6));
+  return n ? (d.Z = n[1] ? 0 : -(n[2] + (n[3] || "00")), i + n[0].length) : -1;
+}
+
+function parseMonthNumber(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.m = n[0] - 1, i + n[0].length) : -1;
+}
+
+function parseDayOfMonth(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.d = +n[0], i + n[0].length) : -1;
+}
+
+function parseDayOfYear(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 3));
+  return n ? (d.m = 0, d.d = +n[0], i + n[0].length) : -1;
+}
+
+function parseHour24(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.H = +n[0], i + n[0].length) : -1;
+}
+
+function parseMinutes(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.M = +n[0], i + n[0].length) : -1;
+}
+
+function parseSeconds(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 2));
+  return n ? (d.S = +n[0], i + n[0].length) : -1;
+}
+
+function parseMilliseconds(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 3));
+  return n ? (d.L = +n[0], i + n[0].length) : -1;
+}
+
+function parseMicroseconds(d, string, i) {
+  var n = numberRe.exec(string.slice(i, i + 6));
+  return n ? (d.L = Math.floor(n[0] / 1000), i + n[0].length) : -1;
+}
+
+function parseLiteralPercent(d, string, i) {
+  var n = percentRe.exec(string.slice(i, i + 1));
+  return n ? i + n[0].length : -1;
+}
+
+function parseUnixTimestamp(d, string, i) {
+  var n = numberRe.exec(string.slice(i));
+  return n ? (d.Q = +n[0], i + n[0].length) : -1;
+}
+
+function parseUnixTimestampSeconds(d, string, i) {
+  var n = numberRe.exec(string.slice(i));
+  return n ? (d.Q = (+n[0]) * 1000, i + n[0].length) : -1;
+}
+
+function formatDayOfMonth(d, p) {
+  return pad$1(d.getDate(), p, 2);
+}
+
+function formatHour24(d, p) {
+  return pad$1(d.getHours(), p, 2);
+}
+
+function formatHour12(d, p) {
+  return pad$1(d.getHours() % 12 || 12, p, 2);
+}
+
+function formatDayOfYear(d, p) {
+  return pad$1(1 + day.count(year(d), d), p, 3);
+}
+
+function formatMilliseconds(d, p) {
+  return pad$1(d.getMilliseconds(), p, 3);
+}
+
+function formatMicroseconds(d, p) {
+  return formatMilliseconds(d, p) + "000";
+}
+
+function formatMonthNumber(d, p) {
+  return pad$1(d.getMonth() + 1, p, 2);
+}
+
+function formatMinutes(d, p) {
+  return pad$1(d.getMinutes(), p, 2);
+}
+
+function formatSeconds(d, p) {
+  return pad$1(d.getSeconds(), p, 2);
+}
+
+function formatWeekdayNumberMonday(d) {
+  var day$$1 = d.getDay();
+  return day$$1 === 0 ? 7 : day$$1;
+}
+
+function formatWeekNumberSunday(d, p) {
+  return pad$1(sunday.count(year(d), d), p, 2);
+}
+
+function formatWeekNumberISO(d, p) {
+  var day$$1 = d.getDay();
+  d = (day$$1 >= 4 || day$$1 === 0) ? thursday(d) : thursday.ceil(d);
+  return pad$1(thursday.count(year(d), d) + (year(d).getDay() === 4), p, 2);
+}
+
+function formatWeekdayNumberSunday(d) {
+  return d.getDay();
+}
+
+function formatWeekNumberMonday(d, p) {
+  return pad$1(monday.count(year(d), d), p, 2);
+}
+
+function formatYear$1(d, p) {
+  return pad$1(d.getFullYear() % 100, p, 2);
+}
+
+function formatFullYear(d, p) {
+  return pad$1(d.getFullYear() % 10000, p, 4);
+}
+
+function formatZone(d) {
+  var z = d.getTimezoneOffset();
+  return (z > 0 ? "-" : (z *= -1, "+"))
+      + pad$1(z / 60 | 0, "0", 2)
+      + pad$1(z % 60, "0", 2);
+}
+
+function formatUTCDayOfMonth(d, p) {
+  return pad$1(d.getUTCDate(), p, 2);
+}
+
+function formatUTCHour24(d, p) {
+  return pad$1(d.getUTCHours(), p, 2);
+}
+
+function formatUTCHour12(d, p) {
+  return pad$1(d.getUTCHours() % 12 || 12, p, 2);
+}
+
+function formatUTCDayOfYear(d, p) {
+  return pad$1(1 + utcDay.count(utcYear(d), d), p, 3);
+}
+
+function formatUTCMilliseconds(d, p) {
+  return pad$1(d.getUTCMilliseconds(), p, 3);
+}
+
+function formatUTCMicroseconds(d, p) {
+  return formatUTCMilliseconds(d, p) + "000";
+}
+
+function formatUTCMonthNumber(d, p) {
+  return pad$1(d.getUTCMonth() + 1, p, 2);
+}
+
+function formatUTCMinutes(d, p) {
+  return pad$1(d.getUTCMinutes(), p, 2);
+}
+
+function formatUTCSeconds(d, p) {
+  return pad$1(d.getUTCSeconds(), p, 2);
+}
+
+function formatUTCWeekdayNumberMonday(d) {
+  var dow = d.getUTCDay();
+  return dow === 0 ? 7 : dow;
+}
+
+function formatUTCWeekNumberSunday(d, p) {
+  return pad$1(utcSunday.count(utcYear(d), d), p, 2);
+}
+
+function formatUTCWeekNumberISO(d, p) {
+  var day$$1 = d.getUTCDay();
+  d = (day$$1 >= 4 || day$$1 === 0) ? utcThursday(d) : utcThursday.ceil(d);
+  return pad$1(utcThursday.count(utcYear(d), d) + (utcYear(d).getUTCDay() === 4), p, 2);
+}
+
+function formatUTCWeekdayNumberSunday(d) {
+  return d.getUTCDay();
+}
+
+function formatUTCWeekNumberMonday(d, p) {
+  return pad$1(utcMonday.count(utcYear(d), d), p, 2);
+}
+
+function formatUTCYear(d, p) {
+  return pad$1(d.getUTCFullYear() % 100, p, 2);
+}
+
+function formatUTCFullYear(d, p) {
+  return pad$1(d.getUTCFullYear() % 10000, p, 4);
+}
+
+function formatUTCZone() {
+  return "+0000";
+}
+
+function formatLiteralPercent() {
+  return "%";
+}
+
+function formatUnixTimestamp(d) {
+  return +d;
+}
+
+function formatUnixTimestampSeconds(d) {
+  return Math.floor(+d / 1000);
+}
+
+var locale$1;
+
+defaultLocale$1({
+  dateTime: "%x, %X",
+  date: "%-m/%-d/%Y",
+  time: "%-I:%M:%S %p",
+  periods: ["AM", "PM"],
+  days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+  shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+  months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+  shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+});
+
+function defaultLocale$1(definition) {
+  locale$1 = formatLocale$1(definition);
+  exports.timeFormat = locale$1.format;
+  exports.timeParse = locale$1.parse;
+  exports.utcFormat = locale$1.utcFormat;
+  exports.utcParse = locale$1.utcParse;
+  return locale$1;
+}
+
+var isoSpecifier = "%Y-%m-%dT%H:%M:%S.%LZ";
+
+function formatIsoNative(date) {
+  return date.toISOString();
+}
+
+var formatIso = Date.prototype.toISOString
+    ? formatIsoNative
+    : exports.utcFormat(isoSpecifier);
+
+function parseIsoNative(string) {
+  var date = new Date(string);
+  return isNaN(date) ? null : date;
+}
+
+var parseIso = +new Date("2000-01-01T00:00:00.000Z")
+    ? parseIsoNative
+    : exports.utcParse(isoSpecifier);
+
+var durationSecond$1 = 1000,
+    durationMinute$1 = durationSecond$1 * 60,
+    durationHour$1 = durationMinute$1 * 60,
+    durationDay$1 = durationHour$1 * 24,
+    durationWeek$1 = durationDay$1 * 7,
+    durationMonth = durationDay$1 * 30,
+    durationYear = durationDay$1 * 365;
+
+function date$1(t) {
+  return new Date(t);
+}
+
+function number$3(t) {
+  return t instanceof Date ? +t : +new Date(+t);
+}
+
+function calendar(year$$1, month$$1, week, day$$1, hour$$1, minute$$1, second$$1, millisecond$$1, format) {
+  var scale = continuous(identity$6, identity$6),
+      invert = scale.invert,
+      domain = scale.domain;
+
+  var formatMillisecond = format(".%L"),
+      formatSecond = format(":%S"),
+      formatMinute = format("%I:%M"),
+      formatHour = format("%I %p"),
+      formatDay = format("%a %d"),
+      formatWeek = format("%b %d"),
+      formatMonth = format("%B"),
+      formatYear = format("%Y");
+
+  var tickIntervals = [
+    [second$$1,  1,      durationSecond$1],
+    [second$$1,  5,  5 * durationSecond$1],
+    [second$$1, 15, 15 * durationSecond$1],
+    [second$$1, 30, 30 * durationSecond$1],
+    [minute$$1,  1,      durationMinute$1],
+    [minute$$1,  5,  5 * durationMinute$1],
+    [minute$$1, 15, 15 * durationMinute$1],
+    [minute$$1, 30, 30 * durationMinute$1],
+    [  hour$$1,  1,      durationHour$1  ],
+    [  hour$$1,  3,  3 * durationHour$1  ],
+    [  hour$$1,  6,  6 * durationHour$1  ],
+    [  hour$$1, 12, 12 * durationHour$1  ],
+    [   day$$1,  1,      durationDay$1   ],
+    [   day$$1,  2,  2 * durationDay$1   ],
+    [  week,  1,      durationWeek$1  ],
+    [ month$$1,  1,      durationMonth ],
+    [ month$$1,  3,  3 * durationMonth ],
+    [  year$$1,  1,      durationYear  ]
+  ];
+
+  function tickFormat(date) {
+    return (second$$1(date) < date ? formatMillisecond
+        : minute$$1(date) < date ? formatSecond
+        : hour$$1(date) < date ? formatMinute
+        : day$$1(date) < date ? formatHour
+        : month$$1(date) < date ? (week(date) < date ? formatDay : formatWeek)
+        : year$$1(date) < date ? formatMonth
+        : formatYear)(date);
+  }
+
+  function tickInterval(interval, start, stop, step) {
+    if (interval == null) interval = 10;
+
+    // If a desired tick count is specified, pick a reasonable tick interval
+    // based on the extent of the domain and a rough estimate of tick size.
+    // Otherwise, assume interval is already a time interval and use it.
+    if (typeof interval === "number") {
+      var target = Math.abs(stop - start) / interval,
+          i = bisector(function(i) { return i[2]; }).right(tickIntervals, target);
+      if (i === tickIntervals.length) {
+        step = tickStep(start / durationYear, stop / durationYear, interval);
+        interval = year$$1;
+      } else if (i) {
+        i = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
+        step = i[1];
+        interval = i[0];
+      } else {
+        step = Math.max(tickStep(start, stop, interval), 1);
+        interval = millisecond$$1;
+      }
+    }
+
+    return step == null ? interval : interval.every(step);
+  }
+
+  scale.invert = function(y) {
+    return new Date(invert(y));
+  };
+
+  scale.domain = function(_) {
+    return arguments.length ? domain(map$2.call(_, number$3)) : domain().map(date$1);
+  };
+
+  scale.ticks = function(interval, step) {
+    var d = domain(),
+        t0 = d[0],
+        t1 = d[d.length - 1],
+        r = t1 < t0,
+        t;
+    if (r) t = t0, t0 = t1, t1 = t;
+    t = tickInterval(interval, t0, t1, step);
+    t = t ? t.range(t0, t1 + 1) : []; // inclusive stop
+    return r ? t.reverse() : t;
+  };
+
+  scale.tickFormat = function(count, specifier) {
+    return specifier == null ? tickFormat : format(specifier);
+  };
+
+  scale.nice = function(interval, step) {
+    var d = domain();
+    return (interval = tickInterval(interval, d[0], d[d.length - 1], step))
+        ? domain(nice(d, interval))
+        : scale;
+  };
+
+  scale.copy = function() {
+    return copy(scale, calendar(year$$1, month$$1, week, day$$1, hour$$1, minute$$1, second$$1, millisecond$$1, format));
+  };
+
+  return scale;
+}
+
+function time() {
+  return initRange.apply(calendar(year, month, sunday, day, hour, minute, second, millisecond, exports.timeFormat).domain([new Date(2000, 0, 1), new Date(2000, 0, 2)]), arguments);
+}
+
+function utcTime() {
+  return initRange.apply(calendar(utcYear, utcMonth, utcSunday, utcDay, utcHour, utcMinute, second, millisecond, exports.utcFormat).domain([Date.UTC(2000, 0, 1), Date.UTC(2000, 0, 2)]), arguments);
+}
+
+function transformer$2() {
+  var x0 = 0,
+      x1 = 1,
+      t0,
+      t1,
+      k10,
+      transform,
+      interpolator = identity$6,
+      clamp = false,
+      unknown;
+
+  function scale(x) {
+    return isNaN(x = +x) ? unknown : interpolator(k10 === 0 ? 0.5 : (x = (transform(x) - t0) * k10, clamp ? Math.max(0, Math.min(1, x)) : x));
+  }
+
+  scale.domain = function(_) {
+    return arguments.length ? (t0 = transform(x0 = +_[0]), t1 = transform(x1 = +_[1]), k10 = t0 === t1 ? 0 : 1 / (t1 - t0), scale) : [x0, x1];
+  };
+
+  scale.clamp = function(_) {
+    return arguments.length ? (clamp = !!_, scale) : clamp;
+  };
+
+  scale.interpolator = function(_) {
+    return arguments.length ? (interpolator = _, scale) : interpolator;
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  return function(t) {
+    transform = t, t0 = t(x0), t1 = t(x1), k10 = t0 === t1 ? 0 : 1 / (t1 - t0);
+    return scale;
+  };
+}
+
+function copy$1(source, target) {
+  return target
+      .domain(source.domain())
+      .interpolator(source.interpolator())
+      .clamp(source.clamp())
+      .unknown(source.unknown());
+}
+
+function sequential() {
+  var scale = linearish(transformer$2()(identity$6));
+
+  scale.copy = function() {
+    return copy$1(scale, sequential());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function sequentialLog() {
+  var scale = loggish(transformer$2()).domain([1, 10]);
+
+  scale.copy = function() {
+    return copy$1(scale, sequentialLog()).base(scale.base());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function sequentialSymlog() {
+  var scale = symlogish(transformer$2());
+
+  scale.copy = function() {
+    return copy$1(scale, sequentialSymlog()).constant(scale.constant());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function sequentialPow() {
+  var scale = powish(transformer$2());
+
+  scale.copy = function() {
+    return copy$1(scale, sequentialPow()).exponent(scale.exponent());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function sequentialSqrt() {
+  return sequentialPow.apply(null, arguments).exponent(0.5);
+}
+
+function sequentialQuantile() {
+  var domain = [],
+      interpolator = identity$6;
+
+  function scale(x) {
+    if (!isNaN(x = +x)) return interpolator((bisectRight(domain, x) - 1) / (domain.length - 1));
+  }
+
+  scale.domain = function(_) {
+    if (!arguments.length) return domain.slice();
+    domain = [];
+    for (var i = 0, n = _.length, d; i < n; ++i) if (d = _[i], d != null && !isNaN(d = +d)) domain.push(d);
+    domain.sort(ascending);
+    return scale;
+  };
+
+  scale.interpolator = function(_) {
+    return arguments.length ? (interpolator = _, scale) : interpolator;
+  };
+
+  scale.copy = function() {
+    return sequentialQuantile(interpolator).domain(domain);
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function transformer$3() {
+  var x0 = 0,
+      x1 = 0.5,
+      x2 = 1,
+      t0,
+      t1,
+      t2,
+      k10,
+      k21,
+      interpolator = identity$6,
+      transform,
+      clamp = false,
+      unknown;
+
+  function scale(x) {
+    return isNaN(x = +x) ? unknown : (x = 0.5 + ((x = +transform(x)) - t1) * (x < t1 ? k10 : k21), interpolator(clamp ? Math.max(0, Math.min(1, x)) : x));
+  }
+
+  scale.domain = function(_) {
+    return arguments.length ? (t0 = transform(x0 = +_[0]), t1 = transform(x1 = +_[1]), t2 = transform(x2 = +_[2]), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1), scale) : [x0, x1, x2];
+  };
+
+  scale.clamp = function(_) {
+    return arguments.length ? (clamp = !!_, scale) : clamp;
+  };
+
+  scale.interpolator = function(_) {
+    return arguments.length ? (interpolator = _, scale) : interpolator;
+  };
+
+  scale.unknown = function(_) {
+    return arguments.length ? (unknown = _, scale) : unknown;
+  };
+
+  return function(t) {
+    transform = t, t0 = t(x0), t1 = t(x1), t2 = t(x2), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1);
+    return scale;
+  };
+}
+
+function diverging() {
+  var scale = linearish(transformer$3()(identity$6));
+
+  scale.copy = function() {
+    return copy$1(scale, diverging());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function divergingLog() {
+  var scale = loggish(transformer$3()).domain([0.1, 1, 10]);
+
+  scale.copy = function() {
+    return copy$1(scale, divergingLog()).base(scale.base());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function divergingSymlog() {
+  var scale = symlogish(transformer$3());
+
+  scale.copy = function() {
+    return copy$1(scale, divergingSymlog()).constant(scale.constant());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function divergingPow() {
+  var scale = powish(transformer$3());
+
+  scale.copy = function() {
+    return copy$1(scale, divergingPow()).exponent(scale.exponent());
+  };
+
+  return initInterpolator.apply(scale, arguments);
+}
+
+function divergingSqrt() {
+  return divergingPow.apply(null, arguments).exponent(0.5);
+}
+
+function colors(specifier) {
+  var n = specifier.length / 6 | 0, colors = new Array(n), i = 0;
+  while (i < n) colors[i] = "#" + specifier.slice(i * 6, ++i * 6);
+  return colors;
+}
+
+var category10 = colors("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf");
+
+var Accent = colors("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666");
+
+var Dark2 = colors("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666");
+
+var Paired = colors("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928");
+
+var Pastel1 = colors("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2");
+
+var Pastel2 = colors("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc");
+
+var Set1 = colors("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999");
+
+var Set2 = colors("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3");
+
+var Set3 = colors("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f");
+
+function ramp(scheme) {
+  return rgbBasis(scheme[scheme.length - 1]);
+}
+
+var scheme = new Array(3).concat(
+  "d8b365f5f5f55ab4ac",
+  "a6611adfc27d80cdc1018571",
+  "a6611adfc27df5f5f580cdc1018571",
+  "8c510ad8b365f6e8c3c7eae55ab4ac01665e",
+  "8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e",
+  "8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e",
+  "8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e",
+  "5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30",
+  "5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30"
+).map(colors);
+
+var BrBG = ramp(scheme);
+
+var scheme$1 = new Array(3).concat(
+  "af8dc3f7f7f77fbf7b",
+  "7b3294c2a5cfa6dba0008837",
+  "7b3294c2a5cff7f7f7a6dba0008837",
+  "762a83af8dc3e7d4e8d9f0d37fbf7b1b7837",
+  "762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837",
+  "762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837",
+  "762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837",
+  "40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b",
+  "40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b"
+).map(colors);
+
+var PRGn = ramp(scheme$1);
+
+var scheme$2 = new Array(3).concat(
+  "e9a3c9f7f7f7a1d76a",
+  "d01c8bf1b6dab8e1864dac26",
+  "d01c8bf1b6daf7f7f7b8e1864dac26",
+  "c51b7de9a3c9fde0efe6f5d0a1d76a4d9221",
+  "c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221",
+  "c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221",
+  "c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221",
+  "8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419",
+  "8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419"
+).map(colors);
+
+var PiYG = ramp(scheme$2);
+
+var scheme$3 = new Array(3).concat(
+  "998ec3f7f7f7f1a340",
+  "5e3c99b2abd2fdb863e66101",
+  "5e3c99b2abd2f7f7f7fdb863e66101",
+  "542788998ec3d8daebfee0b6f1a340b35806",
+  "542788998ec3d8daebf7f7f7fee0b6f1a340b35806",
+  "5427888073acb2abd2d8daebfee0b6fdb863e08214b35806",
+  "5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806",
+  "2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08",
+  "2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08"
+).map(colors);
+
+var PuOr = ramp(scheme$3);
+
+var scheme$4 = new Array(3).concat(
+  "ef8a62f7f7f767a9cf",
+  "ca0020f4a58292c5de0571b0",
+  "ca0020f4a582f7f7f792c5de0571b0",
+  "b2182bef8a62fddbc7d1e5f067a9cf2166ac",
+  "b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac",
+  "b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac",
+  "b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac",
+  "67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061",
+  "67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061"
+).map(colors);
+
+var RdBu = ramp(scheme$4);
+
+var scheme$5 = new Array(3).concat(
+  "ef8a62ffffff999999",
+  "ca0020f4a582bababa404040",
+  "ca0020f4a582ffffffbababa404040",
+  "b2182bef8a62fddbc7e0e0e09999994d4d4d",
+  "b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d",
+  "b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d",
+  "b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d",
+  "67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a",
+  "67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a"
+).map(colors);
+
+var RdGy = ramp(scheme$5);
+
+var scheme$6 = new Array(3).concat(
+  "fc8d59ffffbf91bfdb",
+  "d7191cfdae61abd9e92c7bb6",
+  "d7191cfdae61ffffbfabd9e92c7bb6",
+  "d73027fc8d59fee090e0f3f891bfdb4575b4",
+  "d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4",
+  "d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4",
+  "d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4",
+  "a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695",
+  "a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695"
+).map(colors);
+
+var RdYlBu = ramp(scheme$6);
+
+var scheme$7 = new Array(3).concat(
+  "fc8d59ffffbf91cf60",
+  "d7191cfdae61a6d96a1a9641",
+  "d7191cfdae61ffffbfa6d96a1a9641",
+  "d73027fc8d59fee08bd9ef8b91cf601a9850",
+  "d73027fc8d59fee08bffffbfd9ef8b91cf601a9850",
+  "d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850",
+  "d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850",
+  "a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837",
+  "a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837"
+).map(colors);
+
+var RdYlGn = ramp(scheme$7);
+
+var scheme$8 = new Array(3).concat(
+  "fc8d59ffffbf99d594",
+  "d7191cfdae61abdda42b83ba",
+  "d7191cfdae61ffffbfabdda42b83ba",
+  "d53e4ffc8d59fee08be6f59899d5943288bd",
+  "d53e4ffc8d59fee08bffffbfe6f59899d5943288bd",
+  "d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd",
+  "d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd",
+  "9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2",
+  "9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2"
+).map(colors);
+
+var Spectral = ramp(scheme$8);
+
+var scheme$9 = new Array(3).concat(
+  "e5f5f999d8c92ca25f",
+  "edf8fbb2e2e266c2a4238b45",
+  "edf8fbb2e2e266c2a42ca25f006d2c",
+  "edf8fbccece699d8c966c2a42ca25f006d2c",
+  "edf8fbccece699d8c966c2a441ae76238b45005824",
+  "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824",
+  "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b"
+).map(colors);
+
+var BuGn = ramp(scheme$9);
+
+var scheme$a = new Array(3).concat(
+  "e0ecf49ebcda8856a7",
+  "edf8fbb3cde38c96c688419d",
+  "edf8fbb3cde38c96c68856a7810f7c",
+  "edf8fbbfd3e69ebcda8c96c68856a7810f7c",
+  "edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b",
+  "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b",
+  "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b"
+).map(colors);
+
+var BuPu = ramp(scheme$a);
+
+var scheme$b = new Array(3).concat(
+  "e0f3dba8ddb543a2ca",
+  "f0f9e8bae4bc7bccc42b8cbe",
+  "f0f9e8bae4bc7bccc443a2ca0868ac",
+  "f0f9e8ccebc5a8ddb57bccc443a2ca0868ac",
+  "f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e",
+  "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e",
+  "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081"
+).map(colors);
+
+var GnBu = ramp(scheme$b);
+
+var scheme$c = new Array(3).concat(
+  "fee8c8fdbb84e34a33",
+  "fef0d9fdcc8afc8d59d7301f",
+  "fef0d9fdcc8afc8d59e34a33b30000",
+  "fef0d9fdd49efdbb84fc8d59e34a33b30000",
+  "fef0d9fdd49efdbb84fc8d59ef6548d7301f990000",
+  "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000",
+  "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000"
+).map(colors);
+
+var OrRd = ramp(scheme$c);
+
+var scheme$d = new Array(3).concat(
+  "ece2f0a6bddb1c9099",
+  "f6eff7bdc9e167a9cf02818a",
+  "f6eff7bdc9e167a9cf1c9099016c59",
+  "f6eff7d0d1e6a6bddb67a9cf1c9099016c59",
+  "f6eff7d0d1e6a6bddb67a9cf3690c002818a016450",
+  "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450",
+  "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636"
+).map(colors);
+
+var PuBuGn = ramp(scheme$d);
+
+var scheme$e = new Array(3).concat(
+  "ece7f2a6bddb2b8cbe",
+  "f1eef6bdc9e174a9cf0570b0",
+  "f1eef6bdc9e174a9cf2b8cbe045a8d",
+  "f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d",
+  "f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b",
+  "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b",
+  "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858"
+).map(colors);
+
+var PuBu = ramp(scheme$e);
+
+var scheme$f = new Array(3).concat(
+  "e7e1efc994c7dd1c77",
+  "f1eef6d7b5d8df65b0ce1256",
+  "f1eef6d7b5d8df65b0dd1c77980043",
+  "f1eef6d4b9dac994c7df65b0dd1c77980043",
+  "f1eef6d4b9dac994c7df65b0e7298ace125691003f",
+  "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f",
+  "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f"
+).map(colors);
+
+var PuRd = ramp(scheme$f);
+
+var scheme$g = new Array(3).concat(
+  "fde0ddfa9fb5c51b8a",
+  "feebe2fbb4b9f768a1ae017e",
+  "feebe2fbb4b9f768a1c51b8a7a0177",
+  "feebe2fcc5c0fa9fb5f768a1c51b8a7a0177",
+  "feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177",
+  "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177",
+  "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a"
+).map(colors);
+
+var RdPu = ramp(scheme$g);
+
+var scheme$h = new Array(3).concat(
+  "edf8b17fcdbb2c7fb8",
+  "ffffcca1dab441b6c4225ea8",
+  "ffffcca1dab441b6c42c7fb8253494",
+  "ffffccc7e9b47fcdbb41b6c42c7fb8253494",
+  "ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84",
+  "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84",
+  "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58"
+).map(colors);
+
+var YlGnBu = ramp(scheme$h);
+
+var scheme$i = new Array(3).concat(
+  "f7fcb9addd8e31a354",
+  "ffffccc2e69978c679238443",
+  "ffffccc2e69978c67931a354006837",
+  "ffffccd9f0a3addd8e78c67931a354006837",
+  "ffffccd9f0a3addd8e78c67941ab5d238443005a32",
+  "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32",
+  "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529"
+).map(colors);
+
+var YlGn = ramp(scheme$i);
+
+var scheme$j = new Array(3).concat(
+  "fff7bcfec44fd95f0e",
+  "ffffd4fed98efe9929cc4c02",
+  "ffffd4fed98efe9929d95f0e993404",
+  "ffffd4fee391fec44ffe9929d95f0e993404",
+  "ffffd4fee391fec44ffe9929ec7014cc4c028c2d04",
+  "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04",
+  "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506"
+).map(colors);
+
+var YlOrBr = ramp(scheme$j);
+
+var scheme$k = new Array(3).concat(
+  "ffeda0feb24cf03b20",
+  "ffffb2fecc5cfd8d3ce31a1c",
+  "ffffb2fecc5cfd8d3cf03b20bd0026",
+  "ffffb2fed976feb24cfd8d3cf03b20bd0026",
+  "ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026",
+  "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026",
+  "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026"
+).map(colors);
+
+var YlOrRd = ramp(scheme$k);
+
+var scheme$l = new Array(3).concat(
+  "deebf79ecae13182bd",
+  "eff3ffbdd7e76baed62171b5",
+  "eff3ffbdd7e76baed63182bd08519c",
+  "eff3ffc6dbef9ecae16baed63182bd08519c",
+  "eff3ffc6dbef9ecae16baed64292c62171b5084594",
+  "f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594",
+  "f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b"
+).map(colors);
+
+var Blues = ramp(scheme$l);
+
+var scheme$m = new Array(3).concat(
+  "e5f5e0a1d99b31a354",
+  "edf8e9bae4b374c476238b45",
+  "edf8e9bae4b374c47631a354006d2c",
+  "edf8e9c7e9c0a1d99b74c47631a354006d2c",
+  "edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32",
+  "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32",
+  "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b"
+).map(colors);
+
+var Greens = ramp(scheme$m);
+
+var scheme$n = new Array(3).concat(
+  "f0f0f0bdbdbd636363",
+  "f7f7f7cccccc969696525252",
+  "f7f7f7cccccc969696636363252525",
+  "f7f7f7d9d9d9bdbdbd969696636363252525",
+  "f7f7f7d9d9d9bdbdbd969696737373525252252525",
+  "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525",
+  "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000"
+).map(colors);
+
+var Greys = ramp(scheme$n);
+
+var scheme$o = new Array(3).concat(
+  "efedf5bcbddc756bb1",
+  "f2f0f7cbc9e29e9ac86a51a3",
+  "f2f0f7cbc9e29e9ac8756bb154278f",
+  "f2f0f7dadaebbcbddc9e9ac8756bb154278f",
+  "f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486",
+  "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486",
+  "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d"
+).map(colors);
+
+var Purples = ramp(scheme$o);
+
+var scheme$p = new Array(3).concat(
+  "fee0d2fc9272de2d26",
+  "fee5d9fcae91fb6a4acb181d",
+  "fee5d9fcae91fb6a4ade2d26a50f15",
+  "fee5d9fcbba1fc9272fb6a4ade2d26a50f15",
+  "fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d",
+  "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d",
+  "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d"
+).map(colors);
+
+var Reds = ramp(scheme$p);
+
+var scheme$q = new Array(3).concat(
+  "fee6cefdae6be6550d",
+  "feeddefdbe85fd8d3cd94701",
+  "feeddefdbe85fd8d3ce6550da63603",
+  "feeddefdd0a2fdae6bfd8d3ce6550da63603",
+  "feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04",
+  "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04",
+  "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704"
+).map(colors);
+
+var Oranges = ramp(scheme$q);
+
+var cubehelix$3 = cubehelixLong(cubehelix(300, 0.5, 0.0), cubehelix(-240, 0.5, 1.0));
+
+var warm = cubehelixLong(cubehelix(-100, 0.75, 0.35), cubehelix(80, 1.50, 0.8));
+
+var cool = cubehelixLong(cubehelix(260, 0.75, 0.35), cubehelix(80, 1.50, 0.8));
+
+var c = cubehelix();
+
+function rainbow(t) {
+  if (t < 0 || t > 1) t -= Math.floor(t);
+  var ts = Math.abs(t - 0.5);
+  c.h = 360 * t - 100;
+  c.s = 1.5 - 1.5 * ts;
+  c.l = 0.8 - 0.9 * ts;
+  return c + "";
+}
+
+var c$1 = rgb(),
+    pi_1_3 = Math.PI / 3,
+    pi_2_3 = Math.PI * 2 / 3;
+
+function sinebow(t) {
+  var x;
+  t = (0.5 - t) * Math.PI;
+  c$1.r = 255 * (x = Math.sin(t)) * x;
+  c$1.g = 255 * (x = Math.sin(t + pi_1_3)) * x;
+  c$1.b = 255 * (x = Math.sin(t + pi_2_3)) * x;
+  return c$1 + "";
+}
+
+function ramp$1(range) {
+  var n = range.length;
+  return function(t) {
+    return range[Math.max(0, Math.min(n - 1, Math.floor(t * n)))];
+  };
+}
+
+var viridis = ramp$1(colors("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));
+
+var magma = ramp$1(colors("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf"));
+
+var inferno = ramp$1(colors("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4"));
+
+var plasma = ramp$1(colors("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));
+
+function constant$b(x) {
+  return function constant() {
+    return x;
+  };
+}
+
+var abs$1 = Math.abs;
+var atan2$1 = Math.atan2;
+var cos$2 = Math.cos;
+var max$2 = Math.max;
+var min$1 = Math.min;
+var sin$2 = Math.sin;
+var sqrt$2 = Math.sqrt;
+
+var epsilon$3 = 1e-12;
+var pi$4 = Math.PI;
+var halfPi$3 = pi$4 / 2;
+var tau$4 = 2 * pi$4;
+
+function acos$1(x) {
+  return x > 1 ? 0 : x < -1 ? pi$4 : Math.acos(x);
+}
+
+function asin$1(x) {
+  return x >= 1 ? halfPi$3 : x <= -1 ? -halfPi$3 : Math.asin(x);
+}
+
+function arcInnerRadius(d) {
+  return d.innerRadius;
+}
+
+function arcOuterRadius(d) {
+  return d.outerRadius;
+}
+
+function arcStartAngle(d) {
+  return d.startAngle;
+}
+
+function arcEndAngle(d) {
+  return d.endAngle;
+}
+
+function arcPadAngle(d) {
+  return d && d.padAngle; // Note: optional!
+}
+
+function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
+  var x10 = x1 - x0, y10 = y1 - y0,
+      x32 = x3 - x2, y32 = y3 - y2,
+      t = y32 * x10 - x32 * y10;
+  if (t * t < epsilon$3) return;
+  t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
+  return [x0 + t * x10, y0 + t * y10];
+}
+
+// Compute perpendicular offset line of length rc.
+// http://mathworld.wolfram.com/Circle-LineIntersection.html
+function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
+  var x01 = x0 - x1,
+      y01 = y0 - y1,
+      lo = (cw ? rc : -rc) / sqrt$2(x01 * x01 + y01 * y01),
+      ox = lo * y01,
+      oy = -lo * x01,
+      x11 = x0 + ox,
+      y11 = y0 + oy,
+      x10 = x1 + ox,
+      y10 = y1 + oy,
+      x00 = (x11 + x10) / 2,
+      y00 = (y11 + y10) / 2,
+      dx = x10 - x11,
+      dy = y10 - y11,
+      d2 = dx * dx + dy * dy,
+      r = r1 - rc,
+      D = x11 * y10 - x10 * y11,
+      d = (dy < 0 ? -1 : 1) * sqrt$2(max$2(0, r * r * d2 - D * D)),
+      cx0 = (D * dy - dx * d) / d2,
+      cy0 = (-D * dx - dy * d) / d2,
+      cx1 = (D * dy + dx * d) / d2,
+      cy1 = (-D * dx + dy * d) / d2,
+      dx0 = cx0 - x00,
+      dy0 = cy0 - y00,
+      dx1 = cx1 - x00,
+      dy1 = cy1 - y00;
+
+  // Pick the closer of the two intersection points.
+  // TODO Is there a faster way to determine which intersection to use?
+  if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
+
+  return {
+    cx: cx0,
+    cy: cy0,
+    x01: -ox,
+    y01: -oy,
+    x11: cx0 * (r1 / r - 1),
+    y11: cy0 * (r1 / r - 1)
+  };
+}
+
+function arc() {
+  var innerRadius = arcInnerRadius,
+      outerRadius = arcOuterRadius,
+      cornerRadius = constant$b(0),
+      padRadius = null,
+      startAngle = arcStartAngle,
+      endAngle = arcEndAngle,
+      padAngle = arcPadAngle,
+      context = null;
+
+  function arc() {
+    var buffer,
+        r,
+        r0 = +innerRadius.apply(this, arguments),
+        r1 = +outerRadius.apply(this, arguments),
+        a0 = startAngle.apply(this, arguments) - halfPi$3,
+        a1 = endAngle.apply(this, arguments) - halfPi$3,
+        da = abs$1(a1 - a0),
+        cw = a1 > a0;
+
+    if (!context) context = buffer = path();
+
+    // Ensure that the outer radius is always larger than the inner radius.
+    if (r1 < r0) r = r1, r1 = r0, r0 = r;
+
+    // Is it a point?
+    if (!(r1 > epsilon$3)) context.moveTo(0, 0);
+
+    // Or is it a circle or annulus?
+    else if (da > tau$4 - epsilon$3) {
+      context.moveTo(r1 * cos$2(a0), r1 * sin$2(a0));
+      context.arc(0, 0, r1, a0, a1, !cw);
+      if (r0 > epsilon$3) {
+        context.moveTo(r0 * cos$2(a1), r0 * sin$2(a1));
+        context.arc(0, 0, r0, a1, a0, cw);
+      }
+    }
+
+    // Or is it a circular or annular sector?
+    else {
+      var a01 = a0,
+          a11 = a1,
+          a00 = a0,
+          a10 = a1,
+          da0 = da,
+          da1 = da,
+          ap = padAngle.apply(this, arguments) / 2,
+          rp = (ap > epsilon$3) && (padRadius ? +padRadius.apply(this, arguments) : sqrt$2(r0 * r0 + r1 * r1)),
+          rc = min$1(abs$1(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
+          rc0 = rc,
+          rc1 = rc,
+          t0,
+          t1;
+
+      // Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
+      if (rp > epsilon$3) {
+        var p0 = asin$1(rp / r0 * sin$2(ap)),
+            p1 = asin$1(rp / r1 * sin$2(ap));
+        if ((da0 -= p0 * 2) > epsilon$3) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
+        else da0 = 0, a00 = a10 = (a0 + a1) / 2;
+        if ((da1 -= p1 * 2) > epsilon$3) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
+        else da1 = 0, a01 = a11 = (a0 + a1) / 2;
+      }
+
+      var x01 = r1 * cos$2(a01),
+          y01 = r1 * sin$2(a01),
+          x10 = r0 * cos$2(a10),
+          y10 = r0 * sin$2(a10);
+
+      // Apply rounded corners?
+      if (rc > epsilon$3) {
+        var x11 = r1 * cos$2(a11),
+            y11 = r1 * sin$2(a11),
+            x00 = r0 * cos$2(a00),
+            y00 = r0 * sin$2(a00),
+            oc;
+
+        // Restrict the corner radius according to the sector angle.
+        if (da < pi$4 && (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10))) {
+          var ax = x01 - oc[0],
+              ay = y01 - oc[1],
+              bx = x11 - oc[0],
+              by = y11 - oc[1],
+              kc = 1 / sin$2(acos$1((ax * bx + ay * by) / (sqrt$2(ax * ax + ay * ay) * sqrt$2(bx * bx + by * by))) / 2),
+              lc = sqrt$2(oc[0] * oc[0] + oc[1] * oc[1]);
+          rc0 = min$1(rc, (r0 - lc) / (kc - 1));
+          rc1 = min$1(rc, (r1 - lc) / (kc + 1));
+        }
+      }
+
+      // Is the sector collapsed to a line?
+      if (!(da1 > epsilon$3)) context.moveTo(x01, y01);
+
+      // Does the sector’s outer ring have rounded corners?
+      else if (rc1 > epsilon$3) {
+        t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
+        t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
+
+        context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
+
+        // Have the corners merged?
+        if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2$1(t0.y01, t0.x01), atan2$1(t1.y01, t1.x01), !cw);
+
+        // Otherwise, draw the two corners and the ring.
+        else {
+          context.arc(t0.cx, t0.cy, rc1, atan2$1(t0.y01, t0.x01), atan2$1(t0.y11, t0.x11), !cw);
+          context.arc(0, 0, r1, atan2$1(t0.cy + t0.y11, t0.cx + t0.x11), atan2$1(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
+          context.arc(t1.cx, t1.cy, rc1, atan2$1(t1.y11, t1.x11), atan2$1(t1.y01, t1.x01), !cw);
+        }
+      }
+
+      // Or is the outer ring just a circular arc?
+      else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
+
+      // Is there no inner ring, and it’s a circular sector?
+      // Or perhaps it’s an annular sector collapsed due to padding?
+      if (!(r0 > epsilon$3) || !(da0 > epsilon$3)) context.lineTo(x10, y10);
+
+      // Does the sector’s inner ring (or point) have rounded corners?
+      else if (rc0 > epsilon$3) {
+        t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
+        t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
+
+        context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
+
+        // Have the corners merged?
+        if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2$1(t0.y01, t0.x01), atan2$1(t1.y01, t1.x01), !cw);
+
+        // Otherwise, draw the two corners and the ring.
+        else {
+          context.arc(t0.cx, t0.cy, rc0, atan2$1(t0.y01, t0.x01), atan2$1(t0.y11, t0.x11), !cw);
+          context.arc(0, 0, r0, atan2$1(t0.cy + t0.y11, t0.cx + t0.x11), atan2$1(t1.cy + t1.y11, t1.cx + t1.x11), cw);
+          context.arc(t1.cx, t1.cy, rc0, atan2$1(t1.y11, t1.x11), atan2$1(t1.y01, t1.x01), !cw);
+        }
+      }
+
+      // Or is the inner ring just a circular arc?
+      else context.arc(0, 0, r0, a10, a00, cw);
+    }
+
+    context.closePath();
+
+    if (buffer) return context = null, buffer + "" || null;
+  }
+
+  arc.centroid = function() {
+    var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
+        a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi$4 / 2;
+    return [cos$2(a) * r, sin$2(a) * r];
+  };
+
+  arc.innerRadius = function(_) {
+    return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant$b(+_), arc) : innerRadius;
+  };
+
+  arc.outerRadius = function(_) {
+    return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant$b(+_), arc) : outerRadius;
+  };
+
+  arc.cornerRadius = function(_) {
+    return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant$b(+_), arc) : cornerRadius;
+  };
+
+  arc.padRadius = function(_) {
+    return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant$b(+_), arc) : padRadius;
+  };
+
+  arc.startAngle = function(_) {
+    return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$b(+_), arc) : startAngle;
+  };
+
+  arc.endAngle = function(_) {
+    return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$b(+_), arc) : endAngle;
+  };
+
+  arc.padAngle = function(_) {
+    return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$b(+_), arc) : padAngle;
+  };
+
+  arc.context = function(_) {
+    return arguments.length ? ((context = _ == null ? null : _), arc) : context;
+  };
+
+  return arc;
+}
+
+function Linear(context) {
+  this._context = context;
+}
+
+Linear.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; // proceed
+      default: this._context.lineTo(x, y); break;
+    }
+  }
+};
+
+function curveLinear(context) {
+  return new Linear(context);
+}
+
+function x$3(p) {
+  return p[0];
+}
+
+function y$3(p) {
+  return p[1];
+}
+
+function line() {
+  var x$$1 = x$3,
+      y$$1 = y$3,
+      defined = constant$b(true),
+      context = null,
+      curve = curveLinear,
+      output = null;
+
+  function line(data) {
+    var i,
+        n = data.length,
+        d,
+        defined0 = false,
+        buffer;
+
+    if (context == null) output = curve(buffer = path());
+
+    for (i = 0; i <= n; ++i) {
+      if (!(i < n && defined(d = data[i], i, data)) === defined0) {
+        if (defined0 = !defined0) output.lineStart();
+        else output.lineEnd();
+      }
+      if (defined0) output.point(+x$$1(d, i, data), +y$$1(d, i, data));
+    }
+
+    if (buffer) return output = null, buffer + "" || null;
+  }
+
+  line.x = function(_) {
+    return arguments.length ? (x$$1 = typeof _ === "function" ? _ : constant$b(+_), line) : x$$1;
+  };
+
+  line.y = function(_) {
+    return arguments.length ? (y$$1 = typeof _ === "function" ? _ : constant$b(+_), line) : y$$1;
+  };
+
+  line.defined = function(_) {
+    return arguments.length ? (defined = typeof _ === "function" ? _ : constant$b(!!_), line) : defined;
+  };
+
+  line.curve = function(_) {
+    return arguments.length ? (curve = _, context != null && (output = curve(context)), line) : curve;
+  };
+
+  line.context = function(_) {
+    return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), line) : context;
+  };
+
+  return line;
+}
+
+function area$3() {
+  var x0 = x$3,
+      x1 = null,
+      y0 = constant$b(0),
+      y1 = y$3,
+      defined = constant$b(true),
+      context = null,
+      curve = curveLinear,
+      output = null;
+
+  function area(data) {
+    var i,
+        j,
+        k,
+        n = data.length,
+        d,
+        defined0 = false,
+        buffer,
+        x0z = new Array(n),
+        y0z = new Array(n);
+
+    if (context == null) output = curve(buffer = path());
+
+    for (i = 0; i <= n; ++i) {
+      if (!(i < n && defined(d = data[i], i, data)) === defined0) {
+        if (defined0 = !defined0) {
+          j = i;
+          output.areaStart();
+          output.lineStart();
+        } else {
+          output.lineEnd();
+          output.lineStart();
+          for (k = i - 1; k >= j; --k) {
+            output.point(x0z[k], y0z[k]);
+          }
+          output.lineEnd();
+          output.areaEnd();
+        }
+      }
+      if (defined0) {
+        x0z[i] = +x0(d, i, data), y0z[i] = +y0(d, i, data);
+        output.point(x1 ? +x1(d, i, data) : x0z[i], y1 ? +y1(d, i, data) : y0z[i]);
+      }
+    }
+
+    if (buffer) return output = null, buffer + "" || null;
+  }
+
+  function arealine() {
+    return line().defined(defined).curve(curve).context(context);
+  }
+
+  area.x = function(_) {
+    return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$b(+_), x1 = null, area) : x0;
+  };
+
+  area.x0 = function(_) {
+    return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$b(+_), area) : x0;
+  };
+
+  area.x1 = function(_) {
+    return arguments.length ? (x1 = _ == null ? null : typeof _ === "function" ? _ : constant$b(+_), area) : x1;
+  };
+
+  area.y = function(_) {
+    return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$b(+_), y1 = null, area) : y0;
+  };
+
+  area.y0 = function(_) {
+    return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$b(+_), area) : y0;
+  };
+
+  area.y1 = function(_) {
+    return arguments.length ? (y1 = _ == null ? null : typeof _ === "function" ? _ : constant$b(+_), area) : y1;
+  };
+
+  area.lineX0 =
+  area.lineY0 = function() {
+    return arealine().x(x0).y(y0);
+  };
+
+  area.lineY1 = function() {
+    return arealine().x(x0).y(y1);
+  };
+
+  area.lineX1 = function() {
+    return arealine().x(x1).y(y0);
+  };
+
+  area.defined = function(_) {
+    return arguments.length ? (defined = typeof _ === "function" ? _ : constant$b(!!_), area) : defined;
+  };
+
+  area.curve = function(_) {
+    return arguments.length ? (curve = _, context != null && (output = curve(context)), area) : curve;
+  };
+
+  area.context = function(_) {
+    return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), area) : context;
+  };
+
+  return area;
+}
+
+function descending$1(a, b) {
+  return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+}
+
+function identity$8(d) {
+  return d;
+}
+
+function pie() {
+  var value = identity$8,
+      sortValues = descending$1,
+      sort = null,
+      startAngle = constant$b(0),
+      endAngle = constant$b(tau$4),
+      padAngle = constant$b(0);
+
+  function pie(data) {
+    var i,
+        n = data.length,
+        j,
+        k,
+        sum = 0,
+        index = new Array(n),
+        arcs = new Array(n),
+        a0 = +startAngle.apply(this, arguments),
+        da = Math.min(tau$4, Math.max(-tau$4, endAngle.apply(this, arguments) - a0)),
+        a1,
+        p = Math.min(Math.abs(da) / n, padAngle.apply(this, arguments)),
+        pa = p * (da < 0 ? -1 : 1),
+        v;
+
+    for (i = 0; i < n; ++i) {
+      if ((v = arcs[index[i] = i] = +value(data[i], i, data)) > 0) {
+        sum += v;
+      }
+    }
+
+    // Optionally sort the arcs by previously-computed values or by data.
+    if (sortValues != null) index.sort(function(i, j) { return sortValues(arcs[i], arcs[j]); });
+    else if (sort != null) index.sort(function(i, j) { return sort(data[i], data[j]); });
+
+    // Compute the arcs! They are stored in the original data's order.
+    for (i = 0, k = sum ? (da - n * pa) / sum : 0; i < n; ++i, a0 = a1) {
+      j = index[i], v = arcs[j], a1 = a0 + (v > 0 ? v * k : 0) + pa, arcs[j] = {
+        data: data[j],
+        index: i,
+        value: v,
+        startAngle: a0,
+        endAngle: a1,
+        padAngle: p
+      };
+    }
+
+    return arcs;
+  }
+
+  pie.value = function(_) {
+    return arguments.length ? (value = typeof _ === "function" ? _ : constant$b(+_), pie) : value;
+  };
+
+  pie.sortValues = function(_) {
+    return arguments.length ? (sortValues = _, sort = null, pie) : sortValues;
+  };
+
+  pie.sort = function(_) {
+    return arguments.length ? (sort = _, sortValues = null, pie) : sort;
+  };
+
+  pie.startAngle = function(_) {
+    return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$b(+_), pie) : startAngle;
+  };
+
+  pie.endAngle = function(_) {
+    return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$b(+_), pie) : endAngle;
+  };
+
+  pie.padAngle = function(_) {
+    return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$b(+_), pie) : padAngle;
+  };
+
+  return pie;
+}
+
+var curveRadialLinear = curveRadial(curveLinear);
+
+function Radial(curve) {
+  this._curve = curve;
+}
+
+Radial.prototype = {
+  areaStart: function() {
+    this._curve.areaStart();
+  },
+  areaEnd: function() {
+    this._curve.areaEnd();
+  },
+  lineStart: function() {
+    this._curve.lineStart();
+  },
+  lineEnd: function() {
+    this._curve.lineEnd();
+  },
+  point: function(a, r) {
+    this._curve.point(r * Math.sin(a), r * -Math.cos(a));
+  }
+};
+
+function curveRadial(curve) {
+
+  function radial(context) {
+    return new Radial(curve(context));
+  }
+
+  radial._curve = curve;
+
+  return radial;
+}
+
+function lineRadial(l) {
+  var c = l.curve;
+
+  l.angle = l.x, delete l.x;
+  l.radius = l.y, delete l.y;
+
+  l.curve = function(_) {
+    return arguments.length ? c(curveRadial(_)) : c()._curve;
+  };
+
+  return l;
+}
+
+function lineRadial$1() {
+  return lineRadial(line().curve(curveRadialLinear));
+}
+
+function areaRadial() {
+  var a = area$3().curve(curveRadialLinear),
+      c = a.curve,
+      x0 = a.lineX0,
+      x1 = a.lineX1,
+      y0 = a.lineY0,
+      y1 = a.lineY1;
+
+  a.angle = a.x, delete a.x;
+  a.startAngle = a.x0, delete a.x0;
+  a.endAngle = a.x1, delete a.x1;
+  a.radius = a.y, delete a.y;
+  a.innerRadius = a.y0, delete a.y0;
+  a.outerRadius = a.y1, delete a.y1;
+  a.lineStartAngle = function() { return lineRadial(x0()); }, delete a.lineX0;
+  a.lineEndAngle = function() { return lineRadial(x1()); }, delete a.lineX1;
+  a.lineInnerRadius = function() { return lineRadial(y0()); }, delete a.lineY0;
+  a.lineOuterRadius = function() { return lineRadial(y1()); }, delete a.lineY1;
+
+  a.curve = function(_) {
+    return arguments.length ? c(curveRadial(_)) : c()._curve;
+  };
+
+  return a;
+}
+
+function pointRadial(x, y) {
+  return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
+}
+
+var slice$6 = Array.prototype.slice;
+
+function linkSource(d) {
+  return d.source;
+}
+
+function linkTarget(d) {
+  return d.target;
+}
+
+function link$2(curve) {
+  var source = linkSource,
+      target = linkTarget,
+      x$$1 = x$3,
+      y$$1 = y$3,
+      context = null;
+
+  function link() {
+    var buffer, argv = slice$6.call(arguments), s = source.apply(this, argv), t = target.apply(this, argv);
+    if (!context) context = buffer = path();
+    curve(context, +x$$1.apply(this, (argv[0] = s, argv)), +y$$1.apply(this, argv), +x$$1.apply(this, (argv[0] = t, argv)), +y$$1.apply(this, argv));
+    if (buffer) return context = null, buffer + "" || null;
+  }
+
+  link.source = function(_) {
+    return arguments.length ? (source = _, link) : source;
+  };
+
+  link.target = function(_) {
+    return arguments.length ? (target = _, link) : target;
+  };
+
+  link.x = function(_) {
+    return arguments.length ? (x$$1 = typeof _ === "function" ? _ : constant$b(+_), link) : x$$1;
+  };
+
+  link.y = function(_) {
+    return arguments.length ? (y$$1 = typeof _ === "function" ? _ : constant$b(+_), link) : y$$1;
+  };
+
+  link.context = function(_) {
+    return arguments.length ? ((context = _ == null ? null : _), link) : context;
+  };
+
+  return link;
+}
+
+function curveHorizontal(context, x0, y0, x1, y1) {
+  context.moveTo(x0, y0);
+  context.bezierCurveTo(x0 = (x0 + x1) / 2, y0, x0, y1, x1, y1);
+}
+
+function curveVertical(context, x0, y0, x1, y1) {
+  context.moveTo(x0, y0);
+  context.bezierCurveTo(x0, y0 = (y0 + y1) / 2, x1, y0, x1, y1);
+}
+
+function curveRadial$1(context, x0, y0, x1, y1) {
+  var p0 = pointRadial(x0, y0),
+      p1 = pointRadial(x0, y0 = (y0 + y1) / 2),
+      p2 = pointRadial(x1, y0),
+      p3 = pointRadial(x1, y1);
+  context.moveTo(p0[0], p0[1]);
+  context.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
+}
+
+function linkHorizontal() {
+  return link$2(curveHorizontal);
+}
+
+function linkVertical() {
+  return link$2(curveVertical);
+}
+
+function linkRadial() {
+  var l = link$2(curveRadial$1);
+  l.angle = l.x, delete l.x;
+  l.radius = l.y, delete l.y;
+  return l;
+}
+
+var circle$2 = {
+  draw: function(context, size) {
+    var r = Math.sqrt(size / pi$4);
+    context.moveTo(r, 0);
+    context.arc(0, 0, r, 0, tau$4);
+  }
+};
+
+var cross$2 = {
+  draw: function(context, size) {
+    var r = Math.sqrt(size / 5) / 2;
+    context.moveTo(-3 * r, -r);
+    context.lineTo(-r, -r);
+    context.lineTo(-r, -3 * r);
+    context.lineTo(r, -3 * r);
+    context.lineTo(r, -r);
+    context.lineTo(3 * r, -r);
+    context.lineTo(3 * r, r);
+    context.lineTo(r, r);
+    context.lineTo(r, 3 * r);
+    context.lineTo(-r, 3 * r);
+    context.lineTo(-r, r);
+    context.lineTo(-3 * r, r);
+    context.closePath();
+  }
+};
+
+var tan30 = Math.sqrt(1 / 3),
+    tan30_2 = tan30 * 2;
+
+var diamond = {
+  draw: function(context, size) {
+    var y = Math.sqrt(size / tan30_2),
+        x = y * tan30;
+    context.moveTo(0, -y);
+    context.lineTo(x, 0);
+    context.lineTo(0, y);
+    context.lineTo(-x, 0);
+    context.closePath();
+  }
+};
+
+var ka = 0.89081309152928522810,
+    kr = Math.sin(pi$4 / 10) / Math.sin(7 * pi$4 / 10),
+    kx = Math.sin(tau$4 / 10) * kr,
+    ky = -Math.cos(tau$4 / 10) * kr;
+
+var star = {
+  draw: function(context, size) {
+    var r = Math.sqrt(size * ka),
+        x = kx * r,
+        y = ky * r;
+    context.moveTo(0, -r);
+    context.lineTo(x, y);
+    for (var i = 1; i < 5; ++i) {
+      var a = tau$4 * i / 5,
+          c = Math.cos(a),
+          s = Math.sin(a);
+      context.lineTo(s * r, -c * r);
+      context.lineTo(c * x - s * y, s * x + c * y);
+    }
+    context.closePath();
+  }
+};
+
+var square = {
+  draw: function(context, size) {
+    var w = Math.sqrt(size),
+        x = -w / 2;
+    context.rect(x, x, w, w);
+  }
+};
+
+var sqrt3 = Math.sqrt(3);
+
+var triangle = {
+  draw: function(context, size) {
+    var y = -Math.sqrt(size / (sqrt3 * 3));
+    context.moveTo(0, y * 2);
+    context.lineTo(-sqrt3 * y, -y);
+    context.lineTo(sqrt3 * y, -y);
+    context.closePath();
+  }
+};
+
+var c$2 = -0.5,
+    s = Math.sqrt(3) / 2,
+    k = 1 / Math.sqrt(12),
+    a = (k / 2 + 1) * 3;
+
+var wye = {
+  draw: function(context, size) {
+    var r = Math.sqrt(size / a),
+        x0 = r / 2,
+        y0 = r * k,
+        x1 = x0,
+        y1 = r * k + r,
+        x2 = -x1,
+        y2 = y1;
+    context.moveTo(x0, y0);
+    context.lineTo(x1, y1);
+    context.lineTo(x2, y2);
+    context.lineTo(c$2 * x0 - s * y0, s * x0 + c$2 * y0);
+    context.lineTo(c$2 * x1 - s * y1, s * x1 + c$2 * y1);
+    context.lineTo(c$2 * x2 - s * y2, s * x2 + c$2 * y2);
+    context.lineTo(c$2 * x0 + s * y0, c$2 * y0 - s * x0);
+    context.lineTo(c$2 * x1 + s * y1, c$2 * y1 - s * x1);
+    context.lineTo(c$2 * x2 + s * y2, c$2 * y2 - s * x2);
+    context.closePath();
+  }
+};
+
+var symbols = [
+  circle$2,
+  cross$2,
+  diamond,
+  square,
+  star,
+  triangle,
+  wye
+];
+
+function symbol() {
+  var type = constant$b(circle$2),
+      size = constant$b(64),
+      context = null;
+
+  function symbol() {
+    var buffer;
+    if (!context) context = buffer = path();
+    type.apply(this, arguments).draw(context, +size.apply(this, arguments));
+    if (buffer) return context = null, buffer + "" || null;
+  }
+
+  symbol.type = function(_) {
+    return arguments.length ? (type = typeof _ === "function" ? _ : constant$b(_), symbol) : type;
+  };
+
+  symbol.size = function(_) {
+    return arguments.length ? (size = typeof _ === "function" ? _ : constant$b(+_), symbol) : size;
+  };
+
+  symbol.context = function(_) {
+    return arguments.length ? (context = _ == null ? null : _, symbol) : context;
+  };
+
+  return symbol;
+}
+
+function noop$3() {}
+
+function point$2(that, x, y) {
+  that._context.bezierCurveTo(
+    (2 * that._x0 + that._x1) / 3,
+    (2 * that._y0 + that._y1) / 3,
+    (that._x0 + 2 * that._x1) / 3,
+    (that._y0 + 2 * that._y1) / 3,
+    (that._x0 + 4 * that._x1 + x) / 6,
+    (that._y0 + 4 * that._y1 + y) / 6
+  );
+}
+
+function Basis(context) {
+  this._context = context;
+}
+
+Basis.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 =
+    this._y0 = this._y1 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 3: point$2(this, this._x1, this._y1); // proceed
+      case 2: this._context.lineTo(this._x1, this._y1); break;
+    }
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6); // proceed
+      default: point$2(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = x;
+    this._y0 = this._y1, this._y1 = y;
+  }
+};
+
+function basis$2(context) {
+  return new Basis(context);
+}
+
+function BasisClosed(context) {
+  this._context = context;
+}
+
+BasisClosed.prototype = {
+  areaStart: noop$3,
+  areaEnd: noop$3,
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 = this._x3 = this._x4 =
+    this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 1: {
+        this._context.moveTo(this._x2, this._y2);
+        this._context.closePath();
+        break;
+      }
+      case 2: {
+        this._context.moveTo((this._x2 + 2 * this._x3) / 3, (this._y2 + 2 * this._y3) / 3);
+        this._context.lineTo((this._x3 + 2 * this._x2) / 3, (this._y3 + 2 * this._y2) / 3);
+        this._context.closePath();
+        break;
+      }
+      case 3: {
+        this.point(this._x2, this._y2);
+        this.point(this._x3, this._y3);
+        this.point(this._x4, this._y4);
+        break;
+      }
+    }
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._x2 = x, this._y2 = y; break;
+      case 1: this._point = 2; this._x3 = x, this._y3 = y; break;
+      case 2: this._point = 3; this._x4 = x, this._y4 = y; this._context.moveTo((this._x0 + 4 * this._x1 + x) / 6, (this._y0 + 4 * this._y1 + y) / 6); break;
+      default: point$2(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = x;
+    this._y0 = this._y1, this._y1 = y;
+  }
+};
+
+function basisClosed$1(context) {
+  return new BasisClosed(context);
+}
+
+function BasisOpen(context) {
+  this._context = context;
+}
+
+BasisOpen.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 =
+    this._y0 = this._y1 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; var x0 = (this._x0 + 4 * this._x1 + x) / 6, y0 = (this._y0 + 4 * this._y1 + y) / 6; this._line ? this._context.lineTo(x0, y0) : this._context.moveTo(x0, y0); break;
+      case 3: this._point = 4; // proceed
+      default: point$2(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = x;
+    this._y0 = this._y1, this._y1 = y;
+  }
+};
+
+function basisOpen(context) {
+  return new BasisOpen(context);
+}
+
+function Bundle(context, beta) {
+  this._basis = new Basis(context);
+  this._beta = beta;
+}
+
+Bundle.prototype = {
+  lineStart: function() {
+    this._x = [];
+    this._y = [];
+    this._basis.lineStart();
+  },
+  lineEnd: function() {
+    var x = this._x,
+        y = this._y,
+        j = x.length - 1;
+
+    if (j > 0) {
+      var x0 = x[0],
+          y0 = y[0],
+          dx = x[j] - x0,
+          dy = y[j] - y0,
+          i = -1,
+          t;
+
+      while (++i <= j) {
+        t = i / j;
+        this._basis.point(
+          this._beta * x[i] + (1 - this._beta) * (x0 + t * dx),
+          this._beta * y[i] + (1 - this._beta) * (y0 + t * dy)
+        );
+      }
+    }
+
+    this._x = this._y = null;
+    this._basis.lineEnd();
+  },
+  point: function(x, y) {
+    this._x.push(+x);
+    this._y.push(+y);
+  }
+};
+
+var bundle = (function custom(beta) {
+
+  function bundle(context) {
+    return beta === 1 ? new Basis(context) : new Bundle(context, beta);
+  }
+
+  bundle.beta = function(beta) {
+    return custom(+beta);
+  };
+
+  return bundle;
+})(0.85);
+
+function point$3(that, x, y) {
+  that._context.bezierCurveTo(
+    that._x1 + that._k * (that._x2 - that._x0),
+    that._y1 + that._k * (that._y2 - that._y0),
+    that._x2 + that._k * (that._x1 - x),
+    that._y2 + that._k * (that._y1 - y),
+    that._x2,
+    that._y2
+  );
+}
+
+function Cardinal(context, tension) {
+  this._context = context;
+  this._k = (1 - tension) / 6;
+}
+
+Cardinal.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 =
+    this._y0 = this._y1 = this._y2 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 2: this._context.lineTo(this._x2, this._y2); break;
+      case 3: point$3(this, this._x1, this._y1); break;
+    }
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; this._x1 = x, this._y1 = y; break;
+      case 2: this._point = 3; // proceed
+      default: point$3(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var cardinal = (function custom(tension) {
+
+  function cardinal(context) {
+    return new Cardinal(context, tension);
+  }
+
+  cardinal.tension = function(tension) {
+    return custom(+tension);
+  };
+
+  return cardinal;
+})(0);
+
+function CardinalClosed(context, tension) {
+  this._context = context;
+  this._k = (1 - tension) / 6;
+}
+
+CardinalClosed.prototype = {
+  areaStart: noop$3,
+  areaEnd: noop$3,
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 =
+    this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 1: {
+        this._context.moveTo(this._x3, this._y3);
+        this._context.closePath();
+        break;
+      }
+      case 2: {
+        this._context.lineTo(this._x3, this._y3);
+        this._context.closePath();
+        break;
+      }
+      case 3: {
+        this.point(this._x3, this._y3);
+        this.point(this._x4, this._y4);
+        this.point(this._x5, this._y5);
+        break;
+      }
+    }
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._x3 = x, this._y3 = y; break;
+      case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break;
+      case 2: this._point = 3; this._x5 = x, this._y5 = y; break;
+      default: point$3(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var cardinalClosed = (function custom(tension) {
+
+  function cardinal$$1(context) {
+    return new CardinalClosed(context, tension);
+  }
+
+  cardinal$$1.tension = function(tension) {
+    return custom(+tension);
+  };
+
+  return cardinal$$1;
+})(0);
+
+function CardinalOpen(context, tension) {
+  this._context = context;
+  this._k = (1 - tension) / 6;
+}
+
+CardinalOpen.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 =
+    this._y0 = this._y1 = this._y2 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break;
+      case 3: this._point = 4; // proceed
+      default: point$3(this, x, y); break;
+    }
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var cardinalOpen = (function custom(tension) {
+
+  function cardinal$$1(context) {
+    return new CardinalOpen(context, tension);
+  }
+
+  cardinal$$1.tension = function(tension) {
+    return custom(+tension);
+  };
+
+  return cardinal$$1;
+})(0);
+
+function point$4(that, x, y) {
+  var x1 = that._x1,
+      y1 = that._y1,
+      x2 = that._x2,
+      y2 = that._y2;
+
+  if (that._l01_a > epsilon$3) {
+    var a = 2 * that._l01_2a + 3 * that._l01_a * that._l12_a + that._l12_2a,
+        n = 3 * that._l01_a * (that._l01_a + that._l12_a);
+    x1 = (x1 * a - that._x0 * that._l12_2a + that._x2 * that._l01_2a) / n;
+    y1 = (y1 * a - that._y0 * that._l12_2a + that._y2 * that._l01_2a) / n;
+  }
+
+  if (that._l23_a > epsilon$3) {
+    var b = 2 * that._l23_2a + 3 * that._l23_a * that._l12_a + that._l12_2a,
+        m = 3 * that._l23_a * (that._l23_a + that._l12_a);
+    x2 = (x2 * b + that._x1 * that._l23_2a - x * that._l12_2a) / m;
+    y2 = (y2 * b + that._y1 * that._l23_2a - y * that._l12_2a) / m;
+  }
+
+  that._context.bezierCurveTo(x1, y1, x2, y2, that._x2, that._y2);
+}
+
+function CatmullRom(context, alpha) {
+  this._context = context;
+  this._alpha = alpha;
+}
+
+CatmullRom.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 =
+    this._y0 = this._y1 = this._y2 = NaN;
+    this._l01_a = this._l12_a = this._l23_a =
+    this._l01_2a = this._l12_2a = this._l23_2a =
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 2: this._context.lineTo(this._x2, this._y2); break;
+      case 3: this.point(this._x2, this._y2); break;
+    }
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+
+    if (this._point) {
+      var x23 = this._x2 - x,
+          y23 = this._y2 - y;
+      this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+    }
+
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; // proceed
+      default: point$4(this, x, y); break;
+    }
+
+    this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+    this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var catmullRom = (function custom(alpha) {
+
+  function catmullRom(context) {
+    return alpha ? new CatmullRom(context, alpha) : new Cardinal(context, 0);
+  }
+
+  catmullRom.alpha = function(alpha) {
+    return custom(+alpha);
+  };
+
+  return catmullRom;
+})(0.5);
+
+function CatmullRomClosed(context, alpha) {
+  this._context = context;
+  this._alpha = alpha;
+}
+
+CatmullRomClosed.prototype = {
+  areaStart: noop$3,
+  areaEnd: noop$3,
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 =
+    this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN;
+    this._l01_a = this._l12_a = this._l23_a =
+    this._l01_2a = this._l12_2a = this._l23_2a =
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 1: {
+        this._context.moveTo(this._x3, this._y3);
+        this._context.closePath();
+        break;
+      }
+      case 2: {
+        this._context.lineTo(this._x3, this._y3);
+        this._context.closePath();
+        break;
+      }
+      case 3: {
+        this.point(this._x3, this._y3);
+        this.point(this._x4, this._y4);
+        this.point(this._x5, this._y5);
+        break;
+      }
+    }
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+
+    if (this._point) {
+      var x23 = this._x2 - x,
+          y23 = this._y2 - y;
+      this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+    }
+
+    switch (this._point) {
+      case 0: this._point = 1; this._x3 = x, this._y3 = y; break;
+      case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break;
+      case 2: this._point = 3; this._x5 = x, this._y5 = y; break;
+      default: point$4(this, x, y); break;
+    }
+
+    this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+    this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var catmullRomClosed = (function custom(alpha) {
+
+  function catmullRom$$1(context) {
+    return alpha ? new CatmullRomClosed(context, alpha) : new CardinalClosed(context, 0);
+  }
+
+  catmullRom$$1.alpha = function(alpha) {
+    return custom(+alpha);
+  };
+
+  return catmullRom$$1;
+})(0.5);
+
+function CatmullRomOpen(context, alpha) {
+  this._context = context;
+  this._alpha = alpha;
+}
+
+CatmullRomOpen.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 = this._x2 =
+    this._y0 = this._y1 = this._y2 = NaN;
+    this._l01_a = this._l12_a = this._l23_a =
+    this._l01_2a = this._l12_2a = this._l23_2a =
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+
+    if (this._point) {
+      var x23 = this._x2 - x,
+          y23 = this._y2 - y;
+      this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+    }
+
+    switch (this._point) {
+      case 0: this._point = 1; break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break;
+      case 3: this._point = 4; // proceed
+      default: point$4(this, x, y); break;
+    }
+
+    this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+    this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+    this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+    this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+  }
+};
+
+var catmullRomOpen = (function custom(alpha) {
+
+  function catmullRom$$1(context) {
+    return alpha ? new CatmullRomOpen(context, alpha) : new CardinalOpen(context, 0);
+  }
+
+  catmullRom$$1.alpha = function(alpha) {
+    return custom(+alpha);
+  };
+
+  return catmullRom$$1;
+})(0.5);
+
+function LinearClosed(context) {
+  this._context = context;
+}
+
+LinearClosed.prototype = {
+  areaStart: noop$3,
+  areaEnd: noop$3,
+  lineStart: function() {
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (this._point) this._context.closePath();
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    if (this._point) this._context.lineTo(x, y);
+    else this._point = 1, this._context.moveTo(x, y);
+  }
+};
+
+function linearClosed(context) {
+  return new LinearClosed(context);
+}
+
+function sign$1(x) {
+  return x < 0 ? -1 : 1;
+}
+
+// Calculate the slopes of the tangents (Hermite-type interpolation) based on
+// the following paper: Steffen, M. 1990. A Simple Method for Monotonic
+// Interpolation in One Dimension. Astronomy and Astrophysics, Vol. 239, NO.
+// NOV(II), P. 443, 1990.
+function slope3(that, x2, y2) {
+  var h0 = that._x1 - that._x0,
+      h1 = x2 - that._x1,
+      s0 = (that._y1 - that._y0) / (h0 || h1 < 0 && -0),
+      s1 = (y2 - that._y1) / (h1 || h0 < 0 && -0),
+      p = (s0 * h1 + s1 * h0) / (h0 + h1);
+  return (sign$1(s0) + sign$1(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;
+}
+
+// Calculate a one-sided slope.
+function slope2(that, t) {
+  var h = that._x1 - that._x0;
+  return h ? (3 * (that._y1 - that._y0) / h - t) / 2 : t;
+}
+
+// According to https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Representations
+// "you can express cubic Hermite interpolation in terms of cubic Bézier curves
+// with respect to the four values p0, p0 + m0 / 3, p1 - m1 / 3, p1".
+function point$5(that, t0, t1) {
+  var x0 = that._x0,
+      y0 = that._y0,
+      x1 = that._x1,
+      y1 = that._y1,
+      dx = (x1 - x0) / 3;
+  that._context.bezierCurveTo(x0 + dx, y0 + dx * t0, x1 - dx, y1 - dx * t1, x1, y1);
+}
+
+function MonotoneX(context) {
+  this._context = context;
+}
+
+MonotoneX.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x0 = this._x1 =
+    this._y0 = this._y1 =
+    this._t0 = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    switch (this._point) {
+      case 2: this._context.lineTo(this._x1, this._y1); break;
+      case 3: point$5(this, this._t0, slope2(this, this._t0)); break;
+    }
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    var t1 = NaN;
+
+    x = +x, y = +y;
+    if (x === this._x1 && y === this._y1) return; // Ignore coincident points.
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; break;
+      case 2: this._point = 3; point$5(this, slope2(this, t1 = slope3(this, x, y)), t1); break;
+      default: point$5(this, this._t0, t1 = slope3(this, x, y)); break;
+    }
+
+    this._x0 = this._x1, this._x1 = x;
+    this._y0 = this._y1, this._y1 = y;
+    this._t0 = t1;
+  }
+};
+
+function MonotoneY(context) {
+  this._context = new ReflectContext(context);
+}
+
+(MonotoneY.prototype = Object.create(MonotoneX.prototype)).point = function(x, y) {
+  MonotoneX.prototype.point.call(this, y, x);
+};
+
+function ReflectContext(context) {
+  this._context = context;
+}
+
+ReflectContext.prototype = {
+  moveTo: function(x, y) { this._context.moveTo(y, x); },
+  closePath: function() { this._context.closePath(); },
+  lineTo: function(x, y) { this._context.lineTo(y, x); },
+  bezierCurveTo: function(x1, y1, x2, y2, x, y) { this._context.bezierCurveTo(y1, x1, y2, x2, y, x); }
+};
+
+function monotoneX(context) {
+  return new MonotoneX(context);
+}
+
+function monotoneY(context) {
+  return new MonotoneY(context);
+}
+
+function Natural(context) {
+  this._context = context;
+}
+
+Natural.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x = [];
+    this._y = [];
+  },
+  lineEnd: function() {
+    var x = this._x,
+        y = this._y,
+        n = x.length;
+
+    if (n) {
+      this._line ? this._context.lineTo(x[0], y[0]) : this._context.moveTo(x[0], y[0]);
+      if (n === 2) {
+        this._context.lineTo(x[1], y[1]);
+      } else {
+        var px = controlPoints(x),
+            py = controlPoints(y);
+        for (var i0 = 0, i1 = 1; i1 < n; ++i0, ++i1) {
+          this._context.bezierCurveTo(px[0][i0], py[0][i0], px[1][i0], py[1][i0], x[i1], y[i1]);
+        }
+      }
+    }
+
+    if (this._line || (this._line !== 0 && n === 1)) this._context.closePath();
+    this._line = 1 - this._line;
+    this._x = this._y = null;
+  },
+  point: function(x, y) {
+    this._x.push(+x);
+    this._y.push(+y);
+  }
+};
+
+// See https://www.particleincell.com/2012/bezier-splines/ for derivation.
+function controlPoints(x) {
+  var i,
+      n = x.length - 1,
+      m,
+      a = new Array(n),
+      b = new Array(n),
+      r = new Array(n);
+  a[0] = 0, b[0] = 2, r[0] = x[0] + 2 * x[1];
+  for (i = 1; i < n - 1; ++i) a[i] = 1, b[i] = 4, r[i] = 4 * x[i] + 2 * x[i + 1];
+  a[n - 1] = 2, b[n - 1] = 7, r[n - 1] = 8 * x[n - 1] + x[n];
+  for (i = 1; i < n; ++i) m = a[i] / b[i - 1], b[i] -= m, r[i] -= m * r[i - 1];
+  a[n - 1] = r[n - 1] / b[n - 1];
+  for (i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i];
+  b[n - 1] = (x[n] + a[n - 1]) / 2;
+  for (i = 0; i < n - 1; ++i) b[i] = 2 * x[i + 1] - a[i + 1];
+  return [a, b];
+}
+
+function natural(context) {
+  return new Natural(context);
+}
+
+function Step(context, t) {
+  this._context = context;
+  this._t = t;
+}
+
+Step.prototype = {
+  areaStart: function() {
+    this._line = 0;
+  },
+  areaEnd: function() {
+    this._line = NaN;
+  },
+  lineStart: function() {
+    this._x = this._y = NaN;
+    this._point = 0;
+  },
+  lineEnd: function() {
+    if (0 < this._t && this._t < 1 && this._point === 2) this._context.lineTo(this._x, this._y);
+    if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+    if (this._line >= 0) this._t = 1 - this._t, this._line = 1 - this._line;
+  },
+  point: function(x, y) {
+    x = +x, y = +y;
+    switch (this._point) {
+      case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+      case 1: this._point = 2; // proceed
+      default: {
+        if (this._t <= 0) {
+          this._context.lineTo(this._x, y);
+          this._context.lineTo(x, y);
+        } else {
+          var x1 = this._x * (1 - this._t) + x * this._t;
+          this._context.lineTo(x1, this._y);
+          this._context.lineTo(x1, y);
+        }
+        break;
+      }
+    }
+    this._x = x, this._y = y;
+  }
+};
+
+function step(context) {
+  return new Step(context, 0.5);
+}
+
+function stepBefore(context) {
+  return new Step(context, 0);
+}
+
+function stepAfter(context) {
+  return new Step(context, 1);
+}
+
+function none$1(series, order) {
+  if (!((n = series.length) > 1)) return;
+  for (var i = 1, j, s0, s1 = series[order[0]], n, m = s1.length; i < n; ++i) {
+    s0 = s1, s1 = series[order[i]];
+    for (j = 0; j < m; ++j) {
+      s1[j][1] += s1[j][0] = isNaN(s0[j][1]) ? s0[j][0] : s0[j][1];
+    }
+  }
+}
+
+function none$2(series) {
+  var n = series.length, o = new Array(n);
+  while (--n >= 0) o[n] = n;
+  return o;
+}
+
+function stackValue(d, key) {
+  return d[key];
+}
+
+function stack() {
+  var keys = constant$b([]),
+      order = none$2,
+      offset = none$1,
+      value = stackValue;
+
+  function stack(data) {
+    var kz = keys.apply(this, arguments),
+        i,
+        m = data.length,
+        n = kz.length,
+        sz = new Array(n),
+        oz;
+
+    for (i = 0; i < n; ++i) {
+      for (var ki = kz[i], si = sz[i] = new Array(m), j = 0, sij; j < m; ++j) {
+        si[j] = sij = [0, +value(data[j], ki, j, data)];
+        sij.data = data[j];
+      }
+      si.key = ki;
+    }
+
+    for (i = 0, oz = order(sz); i < n; ++i) {
+      sz[oz[i]].index = i;
+    }
+
+    offset(sz, oz);
+    return sz;
+  }
+
+  stack.keys = function(_) {
+    return arguments.length ? (keys = typeof _ === "function" ? _ : constant$b(slice$6.call(_)), stack) : keys;
+  };
+
+  stack.value = function(_) {
+    return arguments.length ? (value = typeof _ === "function" ? _ : constant$b(+_), stack) : value;
+  };
+
+  stack.order = function(_) {
+    return arguments.length ? (order = _ == null ? none$2 : typeof _ === "function" ? _ : constant$b(slice$6.call(_)), stack) : order;
+  };
+
+  stack.offset = function(_) {
+    return arguments.length ? (offset = _ == null ? none$1 : _, stack) : offset;
+  };
+
+  return stack;
+}
+
+function expand(series, order) {
+  if (!((n = series.length) > 0)) return;
+  for (var i, n, j = 0, m = series[0].length, y; j < m; ++j) {
+    for (y = i = 0; i < n; ++i) y += series[i][j][1] || 0;
+    if (y) for (i = 0; i < n; ++i) series[i][j][1] /= y;
+  }
+  none$1(series, order);
+}
+
+function diverging$1(series, order) {
+  if (!((n = series.length) > 1)) return;
+  for (var i, j = 0, d, dy, yp, yn, n, m = series[order[0]].length; j < m; ++j) {
+    for (yp = yn = 0, i = 0; i < n; ++i) {
+      if ((dy = (d = series[order[i]][j])[1] - d[0]) >= 0) {
+        d[0] = yp, d[1] = yp += dy;
+      } else if (dy < 0) {
+        d[1] = yn, d[0] = yn += dy;
+      } else {
+        d[0] = yp;
+      }
+    }
+  }
+}
+
+function silhouette(series, order) {
+  if (!((n = series.length) > 0)) return;
+  for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) {
+    for (var i = 0, y = 0; i < n; ++i) y += series[i][j][1] || 0;
+    s0[j][1] += s0[j][0] = -y / 2;
+  }
+  none$1(series, order);
+}
+
+function wiggle(series, order) {
+  if (!((n = series.length) > 0) || !((m = (s0 = series[order[0]]).length) > 0)) return;
+  for (var y = 0, j = 1, s0, m, n; j < m; ++j) {
+    for (var i = 0, s1 = 0, s2 = 0; i < n; ++i) {
+      var si = series[order[i]],
+          sij0 = si[j][1] || 0,
+          sij1 = si[j - 1][1] || 0,
+          s3 = (sij0 - sij1) / 2;
+      for (var k = 0; k < i; ++k) {
+        var sk = series[order[k]],
+            skj0 = sk[j][1] || 0,
+            skj1 = sk[j - 1][1] || 0;
+        s3 += skj0 - skj1;
+      }
+      s1 += sij0, s2 += s3 * sij0;
+    }
+    s0[j - 1][1] += s0[j - 1][0] = y;
+    if (s1) y -= s2 / s1;
+  }
+  s0[j - 1][1] += s0[j - 1][0] = y;
+  none$1(series, order);
+}
+
+function appearance(series) {
+  var peaks = series.map(peak);
+  return none$2(series).sort(function(a, b) { return peaks[a] - peaks[b]; });
+}
+
+function peak(series) {
+  var i = -1, j = 0, n = series.length, vi, vj = -Infinity;
+  while (++i < n) if ((vi = +series[i][1]) > vj) vj = vi, j = i;
+  return j;
+}
+
+function ascending$3(series) {
+  var sums = series.map(sum$2);
+  return none$2(series).sort(function(a, b) { return sums[a] - sums[b]; });
+}
+
+function sum$2(series) {
+  var s = 0, i = -1, n = series.length, v;
+  while (++i < n) if (v = +series[i][1]) s += v;
+  return s;
+}
+
+function descending$2(series) {
+  return ascending$3(series).reverse();
+}
+
+function insideOut(series) {
+  var n = series.length,
+      i,
+      j,
+      sums = series.map(sum$2),
+      order = appearance(series),
+      top = 0,
+      bottom = 0,
+      tops = [],
+      bottoms = [];
+
+  for (i = 0; i < n; ++i) {
+    j = order[i];
+    if (top < bottom) {
+      top += sums[j];
+      tops.push(j);
+    } else {
+      bottom += sums[j];
+      bottoms.push(j);
+    }
+  }
+
+  return bottoms.reverse().concat(tops);
+}
+
+function reverse(series) {
+  return none$2(series).reverse();
+}
+
+function constant$c(x) {
+  return function() {
+    return x;
+  };
+}
+
+function x$4(d) {
+  return d[0];
+}
+
+function y$4(d) {
+  return d[1];
+}
+
+function RedBlackTree() {
+  this._ = null; // root node
+}
+
+function RedBlackNode(node) {
+  node.U = // parent node
+  node.C = // color - true for red, false for black
+  node.L = // left node
+  node.R = // right node
+  node.P = // previous node
+  node.N = null; // next node
+}
+
+RedBlackTree.prototype = {
+  constructor: RedBlackTree,
+
+  insert: function(after, node) {
+    var parent, grandpa, uncle;
+
+    if (after) {
+      node.P = after;
+      node.N = after.N;
+      if (after.N) after.N.P = node;
+      after.N = node;
+      if (after.R) {
+        after = after.R;
+        while (after.L) after = after.L;
+        after.L = node;
+      } else {
+        after.R = node;
+      }
+      parent = after;
+    } else if (this._) {
+      after = RedBlackFirst(this._);
+      node.P = null;
+      node.N = after;
+      after.P = after.L = node;
+      parent = after;
+    } else {
+      node.P = node.N = null;
+      this._ = node;
+      parent = null;
+    }
+    node.L = node.R = null;
+    node.U = parent;
+    node.C = true;
+
+    after = node;
+    while (parent && parent.C) {
+      grandpa = parent.U;
+      if (parent === grandpa.L) {
+        uncle = grandpa.R;
+        if (uncle && uncle.C) {
+          parent.C = uncle.C = false;
+          grandpa.C = true;
+          after = grandpa;
+        } else {
+          if (after === parent.R) {
+            RedBlackRotateLeft(this, parent);
+            after = parent;
+            parent = after.U;
+          }
+          parent.C = false;
+          grandpa.C = true;
+          RedBlackRotateRight(this, grandpa);
+        }
+      } else {
+        uncle = grandpa.L;
+        if (uncle && uncle.C) {
+          parent.C = uncle.C = false;
+          grandpa.C = true;
+          after = grandpa;
+        } else {
+          if (after === parent.L) {
+            RedBlackRotateRight(this, parent);
+            after = parent;
+            parent = after.U;
+          }
+          parent.C = false;
+          grandpa.C = true;
+          RedBlackRotateLeft(this, grandpa);
+        }
+      }
+      parent = after.U;
+    }
+    this._.C = false;
+  },
+
+  remove: function(node) {
+    if (node.N) node.N.P = node.P;
+    if (node.P) node.P.N = node.N;
+    node.N = node.P = null;
+
+    var parent = node.U,
+        sibling,
+        left = node.L,
+        right = node.R,
+        next,
+        red;
+
+    if (!left) next = right;
+    else if (!right) next = left;
+    else next = RedBlackFirst(right);
+
+    if (parent) {
+      if (parent.L === node) parent.L = next;
+      else parent.R = next;
+    } else {
+      this._ = next;
+    }
+
+    if (left && right) {
+      red = next.C;
+      next.C = node.C;
+      next.L = left;
+      left.U = next;
+      if (next !== right) {
+        parent = next.U;
+        next.U = node.U;
+        node = next.R;
+        parent.L = node;
+        next.R = right;
+        right.U = next;
+      } else {
+        next.U = parent;
+        parent = next;
+        node = next.R;
+      }
+    } else {
+      red = node.C;
+      node = next;
+    }
+
+    if (node) node.U = parent;
+    if (red) return;
+    if (node && node.C) { node.C = false; return; }
+
+    do {
+      if (node === this._) break;
+      if (node === parent.L) {
+        sibling = parent.R;
+        if (sibling.C) {
+          sibling.C = false;
+          parent.C = true;
+          RedBlackRotateLeft(this, parent);
+          sibling = parent.R;
+        }
+        if ((sibling.L && sibling.L.C)
+            || (sibling.R && sibling.R.C)) {
+          if (!sibling.R || !sibling.R.C) {
+            sibling.L.C = false;
+            sibling.C = true;
+            RedBlackRotateRight(this, sibling);
+            sibling = parent.R;
+          }
+          sibling.C = parent.C;
+          parent.C = sibling.R.C = false;
+          RedBlackRotateLeft(this, parent);
+          node = this._;
+          break;
+        }
+      } else {
+        sibling = parent.L;
+        if (sibling.C) {
+          sibling.C = false;
+          parent.C = true;
+          RedBlackRotateRight(this, parent);
+          sibling = parent.L;
+        }
+        if ((sibling.L && sibling.L.C)
+          || (sibling.R && sibling.R.C)) {
+          if (!sibling.L || !sibling.L.C) {
+            sibling.R.C = false;
+            sibling.C = true;
+            RedBlackRotateLeft(this, sibling);
+            sibling = parent.L;
+          }
+          sibling.C = parent.C;
+          parent.C = sibling.L.C = false;
+          RedBlackRotateRight(this, parent);
+          node = this._;
+          break;
+        }
+      }
+      sibling.C = true;
+      node = parent;
+      parent = parent.U;
+    } while (!node.C);
+
+    if (node) node.C = false;
+  }
+};
+
+function RedBlackRotateLeft(tree, node) {
+  var p = node,
+      q = node.R,
+      parent = p.U;
+
+  if (parent) {
+    if (parent.L === p) parent.L = q;
+    else parent.R = q;
+  } else {
+    tree._ = q;
+  }
+
+  q.U = parent;
+  p.U = q;
+  p.R = q.L;
+  if (p.R) p.R.U = p;
+  q.L = p;
+}
+
+function RedBlackRotateRight(tree, node) {
+  var p = node,
+      q = node.L,
+      parent = p.U;
+
+  if (parent) {
+    if (parent.L === p) parent.L = q;
+    else parent.R = q;
+  } else {
+    tree._ = q;
+  }
+
+  q.U = parent;
+  p.U = q;
+  p.L = q.R;
+  if (p.L) p.L.U = p;
+  q.R = p;
+}
+
+function RedBlackFirst(node) {
+  while (node.L) node = node.L;
+  return node;
+}
+
+function createEdge(left, right, v0, v1) {
+  var edge = [null, null],
+      index = edges.push(edge) - 1;
+  edge.left = left;
+  edge.right = right;
+  if (v0) setEdgeEnd(edge, left, right, v0);
+  if (v1) setEdgeEnd(edge, right, left, v1);
+  cells[left.index].halfedges.push(index);
+  cells[right.index].halfedges.push(index);
+  return edge;
+}
+
+function createBorderEdge(left, v0, v1) {
+  var edge = [v0, v1];
+  edge.left = left;
+  return edge;
+}
+
+function setEdgeEnd(edge, left, right, vertex) {
+  if (!edge[0] && !edge[1]) {
+    edge[0] = vertex;
+    edge.left = left;
+    edge.right = right;
+  } else if (edge.left === right) {
+    edge[1] = vertex;
+  } else {
+    edge[0] = vertex;
+  }
+}
+
+// Liang–Barsky line clipping.
+function clipEdge(edge, x0, y0, x1, y1) {
+  var a = edge[0],
+      b = edge[1],
+      ax = a[0],
+      ay = a[1],
+      bx = b[0],
+      by = b[1],
+      t0 = 0,
+      t1 = 1,
+      dx = bx - ax,
+      dy = by - ay,
+      r;
+
+  r = x0 - ax;
+  if (!dx && r > 0) return;
+  r /= dx;
+  if (dx < 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  } else if (dx > 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  }
+
+  r = x1 - ax;
+  if (!dx && r < 0) return;
+  r /= dx;
+  if (dx < 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  } else if (dx > 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  }
+
+  r = y0 - ay;
+  if (!dy && r > 0) return;
+  r /= dy;
+  if (dy < 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  } else if (dy > 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  }
+
+  r = y1 - ay;
+  if (!dy && r < 0) return;
+  r /= dy;
+  if (dy < 0) {
+    if (r > t1) return;
+    if (r > t0) t0 = r;
+  } else if (dy > 0) {
+    if (r < t0) return;
+    if (r < t1) t1 = r;
+  }
+
+  if (!(t0 > 0) && !(t1 < 1)) return true; // TODO Better check?
+
+  if (t0 > 0) edge[0] = [ax + t0 * dx, ay + t0 * dy];
+  if (t1 < 1) edge[1] = [ax + t1 * dx, ay + t1 * dy];
+  return true;
+}
+
+function connectEdge(edge, x0, y0, x1, y1) {
+  var v1 = edge[1];
+  if (v1) return true;
+
+  var v0 = edge[0],
+      left = edge.left,
+      right = edge.right,
+      lx = left[0],
+      ly = left[1],
+      rx = right[0],
+      ry = right[1],
+      fx = (lx + rx) / 2,
+      fy = (ly + ry) / 2,
+      fm,
+      fb;
+
+  if (ry === ly) {
+    if (fx < x0 || fx >= x1) return;
+    if (lx > rx) {
+      if (!v0) v0 = [fx, y0];
+      else if (v0[1] >= y1) return;
+      v1 = [fx, y1];
+    } else {
+      if (!v0) v0 = [fx, y1];
+      else if (v0[1] < y0) return;
+      v1 = [fx, y0];
+    }
+  } else {
+    fm = (lx - rx) / (ry - ly);
+    fb = fy - fm * fx;
+    if (fm < -1 || fm > 1) {
+      if (lx > rx) {
+        if (!v0) v0 = [(y0 - fb) / fm, y0];
+        else if (v0[1] >= y1) return;
+        v1 = [(y1 - fb) / fm, y1];
+      } else {
+        if (!v0) v0 = [(y1 - fb) / fm, y1];
+        else if (v0[1] < y0) return;
+        v1 = [(y0 - fb) / fm, y0];
+      }
+    } else {
+      if (ly < ry) {
+        if (!v0) v0 = [x0, fm * x0 + fb];
+        else if (v0[0] >= x1) return;
+        v1 = [x1, fm * x1 + fb];
+      } else {
+        if (!v0) v0 = [x1, fm * x1 + fb];
+        else if (v0[0] < x0) return;
+        v1 = [x0, fm * x0 + fb];
+      }
+    }
+  }
+
+  edge[0] = v0;
+  edge[1] = v1;
+  return true;
+}
+
+function clipEdges(x0, y0, x1, y1) {
+  var i = edges.length,
+      edge;
+
+  while (i--) {
+    if (!connectEdge(edge = edges[i], x0, y0, x1, y1)
+        || !clipEdge(edge, x0, y0, x1, y1)
+        || !(Math.abs(edge[0][0] - edge[1][0]) > epsilon$4
+            || Math.abs(edge[0][1] - edge[1][1]) > epsilon$4)) {
+      delete edges[i];
+    }
+  }
+}
+
+function createCell(site) {
+  return cells[site.index] = {
+    site: site,
+    halfedges: []
+  };
+}
+
+function cellHalfedgeAngle(cell, edge) {
+  var site = cell.site,
+      va = edge.left,
+      vb = edge.right;
+  if (site === vb) vb = va, va = site;
+  if (vb) return Math.atan2(vb[1] - va[1], vb[0] - va[0]);
+  if (site === va) va = edge[1], vb = edge[0];
+  else va = edge[0], vb = edge[1];
+  return Math.atan2(va[0] - vb[0], vb[1] - va[1]);
+}
+
+function cellHalfedgeStart(cell, edge) {
+  return edge[+(edge.left !== cell.site)];
+}
+
+function cellHalfedgeEnd(cell, edge) {
+  return edge[+(edge.left === cell.site)];
+}
+
+function sortCellHalfedges() {
+  for (var i = 0, n = cells.length, cell, halfedges, j, m; i < n; ++i) {
+    if ((cell = cells[i]) && (m = (halfedges = cell.halfedges).length)) {
+      var index = new Array(m),
+          array = new Array(m);
+      for (j = 0; j < m; ++j) index[j] = j, array[j] = cellHalfedgeAngle(cell, edges[halfedges[j]]);
+      index.sort(function(i, j) { return array[j] - array[i]; });
+      for (j = 0; j < m; ++j) array[j] = halfedges[index[j]];
+      for (j = 0; j < m; ++j) halfedges[j] = array[j];
+    }
+  }
+}
+
+function clipCells(x0, y0, x1, y1) {
+  var nCells = cells.length,
+      iCell,
+      cell,
+      site,
+      iHalfedge,
+      halfedges,
+      nHalfedges,
+      start,
+      startX,
+      startY,
+      end,
+      endX,
+      endY,
+      cover = true;
+
+  for (iCell = 0; iCell < nCells; ++iCell) {
+    if (cell = cells[iCell]) {
+      site = cell.site;
+      halfedges = cell.halfedges;
+      iHalfedge = halfedges.length;
+
+      // Remove any dangling clipped edges.
+      while (iHalfedge--) {
+        if (!edges[halfedges[iHalfedge]]) {
+          halfedges.splice(iHalfedge, 1);
+        }
+      }
+
+      // Insert any border edges as necessary.
+      iHalfedge = 0, nHalfedges = halfedges.length;
+      while (iHalfedge < nHalfedges) {
+        end = cellHalfedgeEnd(cell, edges[halfedges[iHalfedge]]), endX = end[0], endY = end[1];
+        start = cellHalfedgeStart(cell, edges[halfedges[++iHalfedge % nHalfedges]]), startX = start[0], startY = start[1];
+        if (Math.abs(endX - startX) > epsilon$4 || Math.abs(endY - startY) > epsilon$4) {
+          halfedges.splice(iHalfedge, 0, edges.push(createBorderEdge(site, end,
+              Math.abs(endX - x0) < epsilon$4 && y1 - endY > epsilon$4 ? [x0, Math.abs(startX - x0) < epsilon$4 ? startY : y1]
+              : Math.abs(endY - y1) < epsilon$4 && x1 - endX > epsilon$4 ? [Math.abs(startY - y1) < epsilon$4 ? startX : x1, y1]
+              : Math.abs(endX - x1) < epsilon$4 && endY - y0 > epsilon$4 ? [x1, Math.abs(startX - x1) < epsilon$4 ? startY : y0]
+              : Math.abs(endY - y0) < epsilon$4 && endX - x0 > epsilon$4 ? [Math.abs(startY - y0) < epsilon$4 ? startX : x0, y0]
+              : null)) - 1);
+          ++nHalfedges;
+        }
+      }
+
+      if (nHalfedges) cover = false;
+    }
+  }
+
+  // If there weren’t any edges, have the closest site cover the extent.
+  // It doesn’t matter which corner of the extent we measure!
+  if (cover) {
+    var dx, dy, d2, dc = Infinity;
+
+    for (iCell = 0, cover = null; iCell < nCells; ++iCell) {
+      if (cell = cells[iCell]) {
+        site = cell.site;
+        dx = site[0] - x0;
+        dy = site[1] - y0;
+        d2 = dx * dx + dy * dy;
+        if (d2 < dc) dc = d2, cover = cell;
+      }
+    }
+
+    if (cover) {
+      var v00 = [x0, y0], v01 = [x0, y1], v11 = [x1, y1], v10 = [x1, y0];
+      cover.halfedges.push(
+        edges.push(createBorderEdge(site = cover.site, v00, v01)) - 1,
+        edges.push(createBorderEdge(site, v01, v11)) - 1,
+        edges.push(createBorderEdge(site, v11, v10)) - 1,
+        edges.push(createBorderEdge(site, v10, v00)) - 1
+      );
+    }
+  }
+
+  // Lastly delete any cells with no edges; these were entirely clipped.
+  for (iCell = 0; iCell < nCells; ++iCell) {
+    if (cell = cells[iCell]) {
+      if (!cell.halfedges.length) {
+        delete cells[iCell];
+      }
+    }
+  }
+}
+
+var circlePool = [];
+
+var firstCircle;
+
+function Circle() {
+  RedBlackNode(this);
+  this.x =
+  this.y =
+  this.arc =
+  this.site =
+  this.cy = null;
+}
+
+function attachCircle(arc) {
+  var lArc = arc.P,
+      rArc = arc.N;
+
+  if (!lArc || !rArc) return;
+
+  var lSite = lArc.site,
+      cSite = arc.site,
+      rSite = rArc.site;
+
+  if (lSite === rSite) return;
+
+  var bx = cSite[0],
+      by = cSite[1],
+      ax = lSite[0] - bx,
+      ay = lSite[1] - by,
+      cx = rSite[0] - bx,
+      cy = rSite[1] - by;
+
+  var d = 2 * (ax * cy - ay * cx);
+  if (d >= -epsilon2$2) return;
+
+  var ha = ax * ax + ay * ay,
+      hc = cx * cx + cy * cy,
+      x = (cy * ha - ay * hc) / d,
+      y = (ax * hc - cx * ha) / d;
+
+  var circle = circlePool.pop() || new Circle;
+  circle.arc = arc;
+  circle.site = cSite;
+  circle.x = x + bx;
+  circle.y = (circle.cy = y + by) + Math.sqrt(x * x + y * y); // y bottom
+
+  arc.circle = circle;
+
+  var before = null,
+      node = circles._;
+
+  while (node) {
+    if (circle.y < node.y || (circle.y === node.y && circle.x <= node.x)) {
+      if (node.L) node = node.L;
+      else { before = node.P; break; }
+    } else {
+      if (node.R) node = node.R;
+      else { before = node; break; }
+    }
+  }
+
+  circles.insert(before, circle);
+  if (!before) firstCircle = circle;
+}
+
+function detachCircle(arc) {
+  var circle = arc.circle;
+  if (circle) {
+    if (!circle.P) firstCircle = circle.N;
+    circles.remove(circle);
+    circlePool.push(circle);
+    RedBlackNode(circle);
+    arc.circle = null;
+  }
+}
+
+var beachPool = [];
+
+function Beach() {
+  RedBlackNode(this);
+  this.edge =
+  this.site =
+  this.circle = null;
+}
+
+function createBeach(site) {
+  var beach = beachPool.pop() || new Beach;
+  beach.site = site;
+  return beach;
+}
+
+function detachBeach(beach) {
+  detachCircle(beach);
+  beaches.remove(beach);
+  beachPool.push(beach);
+  RedBlackNode(beach);
+}
+
+function removeBeach(beach) {
+  var circle = beach.circle,
+      x = circle.x,
+      y = circle.cy,
+      vertex = [x, y],
+      previous = beach.P,
+      next = beach.N,
+      disappearing = [beach];
+
+  detachBeach(beach);
+
+  var lArc = previous;
+  while (lArc.circle
+      && Math.abs(x - lArc.circle.x) < epsilon$4
+      && Math.abs(y - lArc.circle.cy) < epsilon$4) {
+    previous = lArc.P;
+    disappearing.unshift(lArc);
+    detachBeach(lArc);
+    lArc = previous;
+  }
+
+  disappearing.unshift(lArc);
+  detachCircle(lArc);
+
+  var rArc = next;
+  while (rArc.circle
+      && Math.abs(x - rArc.circle.x) < epsilon$4
+      && Math.abs(y - rArc.circle.cy) < epsilon$4) {
+    next = rArc.N;
+    disappearing.push(rArc);
+    detachBeach(rArc);
+    rArc = next;
+  }
+
+  disappearing.push(rArc);
+  detachCircle(rArc);
+
+  var nArcs = disappearing.length,
+      iArc;
+  for (iArc = 1; iArc < nArcs; ++iArc) {
+    rArc = disappearing[iArc];
+    lArc = disappearing[iArc - 1];
+    setEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex);
+  }
+
+  lArc = disappearing[0];
+  rArc = disappearing[nArcs - 1];
+  rArc.edge = createEdge(lArc.site, rArc.site, null, vertex);
+
+  attachCircle(lArc);
+  attachCircle(rArc);
+}
+
+function addBeach(site) {
+  var x = site[0],
+      directrix = site[1],
+      lArc,
+      rArc,
+      dxl,
+      dxr,
+      node = beaches._;
+
+  while (node) {
+    dxl = leftBreakPoint(node, directrix) - x;
+    if (dxl > epsilon$4) node = node.L; else {
+      dxr = x - rightBreakPoint(node, directrix);
+      if (dxr > epsilon$4) {
+        if (!node.R) {
+          lArc = node;
+          break;
+        }
+        node = node.R;
+      } else {
+        if (dxl > -epsilon$4) {
+          lArc = node.P;
+          rArc = node;
+        } else if (dxr > -epsilon$4) {
+          lArc = node;
+          rArc = node.N;
+        } else {
+          lArc = rArc = node;
+        }
+        break;
+      }
+    }
+  }
+
+  createCell(site);
+  var newArc = createBeach(site);
+  beaches.insert(lArc, newArc);
+
+  if (!lArc && !rArc) return;
+
+  if (lArc === rArc) {
+    detachCircle(lArc);
+    rArc = createBeach(lArc.site);
+    beaches.insert(newArc, rArc);
+    newArc.edge = rArc.edge = createEdge(lArc.site, newArc.site);
+    attachCircle(lArc);
+    attachCircle(rArc);
+    return;
+  }
+
+  if (!rArc) { // && lArc
+    newArc.edge = createEdge(lArc.site, newArc.site);
+    return;
+  }
+
+  // else lArc !== rArc
+  detachCircle(lArc);
+  detachCircle(rArc);
+
+  var lSite = lArc.site,
+      ax = lSite[0],
+      ay = lSite[1],
+      bx = site[0] - ax,
+      by = site[1] - ay,
+      rSite = rArc.site,
+      cx = rSite[0] - ax,
+      cy = rSite[1] - ay,
+      d = 2 * (bx * cy - by * cx),
+      hb = bx * bx + by * by,
+      hc = cx * cx + cy * cy,
+      vertex = [(cy * hb - by * hc) / d + ax, (bx * hc - cx * hb) / d + ay];
+
+  setEdgeEnd(rArc.edge, lSite, rSite, vertex);
+  newArc.edge = createEdge(lSite, site, null, vertex);
+  rArc.edge = createEdge(site, rSite, null, vertex);
+  attachCircle(lArc);
+  attachCircle(rArc);
+}
+
+function leftBreakPoint(arc, directrix) {
+  var site = arc.site,
+      rfocx = site[0],
+      rfocy = site[1],
+      pby2 = rfocy - directrix;
+
+  if (!pby2) return rfocx;
+
+  var lArc = arc.P;
+  if (!lArc) return -Infinity;
+
+  site = lArc.site;
+  var lfocx = site[0],
+      lfocy = site[1],
+      plby2 = lfocy - directrix;
+
+  if (!plby2) return lfocx;
+
+  var hl = lfocx - rfocx,
+      aby2 = 1 / pby2 - 1 / plby2,
+      b = hl / plby2;
+
+  if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx;
+
+  return (rfocx + lfocx) / 2;
+}
+
+function rightBreakPoint(arc, directrix) {
+  var rArc = arc.N;
+  if (rArc) return leftBreakPoint(rArc, directrix);
+  var site = arc.site;
+  return site[1] === directrix ? site[0] : Infinity;
+}
+
+var epsilon$4 = 1e-6;
+var epsilon2$2 = 1e-12;
+var beaches;
+var cells;
+var circles;
+var edges;
+
+function triangleArea(a, b, c) {
+  return (a[0] - c[0]) * (b[1] - a[1]) - (a[0] - b[0]) * (c[1] - a[1]);
+}
+
+function lexicographic(a, b) {
+  return b[1] - a[1]
+      || b[0] - a[0];
+}
+
+function Diagram(sites, extent) {
+  var site = sites.sort(lexicographic).pop(),
+      x,
+      y,
+      circle;
+
+  edges = [];
+  cells = new Array(sites.length);
+  beaches = new RedBlackTree;
+  circles = new RedBlackTree;
+
+  while (true) {
+    circle = firstCircle;
+    if (site && (!circle || site[1] < circle.y || (site[1] === circle.y && site[0] < circle.x))) {
+      if (site[0] !== x || site[1] !== y) {
+        addBeach(site);
+        x = site[0], y = site[1];
+      }
+      site = sites.pop();
+    } else if (circle) {
+      removeBeach(circle.arc);
+    } else {
+      break;
+    }
+  }
+
+  sortCellHalfedges();
+
+  if (extent) {
+    var x0 = +extent[0][0],
+        y0 = +extent[0][1],
+        x1 = +extent[1][0],
+        y1 = +extent[1][1];
+    clipEdges(x0, y0, x1, y1);
+    clipCells(x0, y0, x1, y1);
+  }
+
+  this.edges = edges;
+  this.cells = cells;
+
+  beaches =
+  circles =
+  edges =
+  cells = null;
+}
+
+Diagram.prototype = {
+  constructor: Diagram,
+
+  polygons: function() {
+    var edges = this.edges;
+
+    return this.cells.map(function(cell) {
+      var polygon = cell.halfedges.map(function(i) { return cellHalfedgeStart(cell, edges[i]); });
+      polygon.data = cell.site.data;
+      return polygon;
+    });
+  },
+
+  triangles: function() {
+    var triangles = [],
+        edges = this.edges;
+
+    this.cells.forEach(function(cell, i) {
+      if (!(m = (halfedges = cell.halfedges).length)) return;
+      var site = cell.site,
+          halfedges,
+          j = -1,
+          m,
+          s0,
+          e1 = edges[halfedges[m - 1]],
+          s1 = e1.left === site ? e1.right : e1.left;
+
+      while (++j < m) {
+        s0 = s1;
+        e1 = edges[halfedges[j]];
+        s1 = e1.left === site ? e1.right : e1.left;
+        if (s0 && s1 && i < s0.index && i < s1.index && triangleArea(site, s0, s1) < 0) {
+          triangles.push([site.data, s0.data, s1.data]);
+        }
+      }
+    });
+
+    return triangles;
+  },
+
+  links: function() {
+    return this.edges.filter(function(edge) {
+      return edge.right;
+    }).map(function(edge) {
+      return {
+        source: edge.left.data,
+        target: edge.right.data
+      };
+    });
+  },
+
+  find: function(x, y, radius) {
+    var that = this, i0, i1 = that._found || 0, n = that.cells.length, cell;
+
+    // Use the previously-found cell, or start with an arbitrary one.
+    while (!(cell = that.cells[i1])) if (++i1 >= n) return null;
+    var dx = x - cell.site[0], dy = y - cell.site[1], d2 = dx * dx + dy * dy;
+
+    // Traverse the half-edges to find a closer cell, if any.
+    do {
+      cell = that.cells[i0 = i1], i1 = null;
+      cell.halfedges.forEach(function(e) {
+        var edge = that.edges[e], v = edge.left;
+        if ((v === cell.site || !v) && !(v = edge.right)) return;
+        var vx = x - v[0], vy = y - v[1], v2 = vx * vx + vy * vy;
+        if (v2 < d2) d2 = v2, i1 = v.index;
+      });
+    } while (i1 !== null);
+
+    that._found = i0;
+
+    return radius == null || d2 <= radius * radius ? cell.site : null;
+  }
+};
+
+function voronoi() {
+  var x$$1 = x$4,
+      y$$1 = y$4,
+      extent = null;
+
+  function voronoi(data) {
+    return new Diagram(data.map(function(d, i) {
+      var s = [Math.round(x$$1(d, i, data) / epsilon$4) * epsilon$4, Math.round(y$$1(d, i, data) / epsilon$4) * epsilon$4];
+      s.index = i;
+      s.data = d;
+      return s;
+    }), extent);
+  }
+
+  voronoi.polygons = function(data) {
+    return voronoi(data).polygons();
+  };
+
+  voronoi.links = function(data) {
+    return voronoi(data).links();
+  };
+
+  voronoi.triangles = function(data) {
+    return voronoi(data).triangles();
+  };
+
+  voronoi.x = function(_) {
+    return arguments.length ? (x$$1 = typeof _ === "function" ? _ : constant$c(+_), voronoi) : x$$1;
+  };
+
+  voronoi.y = function(_) {
+    return arguments.length ? (y$$1 = typeof _ === "function" ? _ : constant$c(+_), voronoi) : y$$1;
+  };
+
+  voronoi.extent = function(_) {
+    return arguments.length ? (extent = _ == null ? null : [[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]], voronoi) : extent && [[extent[0][0], extent[0][1]], [extent[1][0], extent[1][1]]];
+  };
+
+  voronoi.size = function(_) {
+    return arguments.length ? (extent = _ == null ? null : [[0, 0], [+_[0], +_[1]]], voronoi) : extent && [extent[1][0] - extent[0][0], extent[1][1] - extent[0][1]];
+  };
+
+  return voronoi;
+}
+
+function constant$d(x) {
+  return function() {
+    return x;
+  };
+}
+
+function ZoomEvent(target, type, transform) {
+  this.target = target;
+  this.type = type;
+  this.transform = transform;
+}
+
+function Transform(k, x, y) {
+  this.k = k;
+  this.x = x;
+  this.y = y;
+}
+
+Transform.prototype = {
+  constructor: Transform,
+  scale: function(k) {
+    return k === 1 ? this : new Transform(this.k * k, this.x, this.y);
+  },
+  translate: function(x, y) {
+    return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);
+  },
+  apply: function(point) {
+    return [point[0] * this.k + this.x, point[1] * this.k + this.y];
+  },
+  applyX: function(x) {
+    return x * this.k + this.x;
+  },
+  applyY: function(y) {
+    return y * this.k + this.y;
+  },
+  invert: function(location) {
+    return [(location[0] - this.x) / this.k, (location[1] - this.y) / this.k];
+  },
+  invertX: function(x) {
+    return (x - this.x) / this.k;
+  },
+  invertY: function(y) {
+    return (y - this.y) / this.k;
+  },
+  rescaleX: function(x) {
+    return x.copy().domain(x.range().map(this.invertX, this).map(x.invert, x));
+  },
+  rescaleY: function(y) {
+    return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y));
+  },
+  toString: function() {
+    return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
+  }
+};
+
+var identity$9 = new Transform(1, 0, 0);
+
+transform$1.prototype = Transform.prototype;
+
+function transform$1(node) {
+  return node.__zoom || identity$9;
+}
+
+function nopropagation$2() {
+  exports.event.stopImmediatePropagation();
+}
+
+function noevent$2() {
+  exports.event.preventDefault();
+  exports.event.stopImmediatePropagation();
+}
+
+// Ignore right-click, since that should open the context menu.
+function defaultFilter$2() {
+  return !exports.event.button;
+}
+
+function defaultExtent$1() {
+  var e = this, w, h;
+  if (e instanceof SVGElement) {
+    e = e.ownerSVGElement || e;
+    w = e.width.baseVal.value;
+    h = e.height.baseVal.value;
+  } else {
+    w = e.clientWidth;
+    h = e.clientHeight;
+  }
+  return [[0, 0], [w, h]];
+}
+
+function defaultTransform() {
+  return this.__zoom || identity$9;
+}
+
+function defaultWheelDelta() {
+  return -exports.event.deltaY * (exports.event.deltaMode ? 120 : 1) / 500;
+}
+
+function defaultTouchable$1() {
+  return "ontouchstart" in this;
+}
+
+function defaultConstrain(transform, extent, translateExtent) {
+  var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],
+      dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],
+      dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],
+      dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];
+  return transform.translate(
+    dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
+    dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
+  );
+}
+
+function zoom() {
+  var filter = defaultFilter$2,
+      extent = defaultExtent$1,
+      constrain = defaultConstrain,
+      wheelDelta = defaultWheelDelta,
+      touchable = defaultTouchable$1,
+      scaleExtent = [0, Infinity],
+      translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],
+      duration = 250,
+      interpolate = interpolateZoom,
+      gestures = [],
+      listeners = dispatch("start", "zoom", "end"),
+      touchstarting,
+      touchending,
+      touchDelay = 500,
+      wheelDelay = 150,
+      clickDistance2 = 0;
+
+  function zoom(selection$$1) {
+    selection$$1
+        .property("__zoom", defaultTransform)
+        .on("wheel.zoom", wheeled)
+        .on("mousedown.zoom", mousedowned)
+        .on("dblclick.zoom", dblclicked)
+      .filter(touchable)
+        .on("touchstart.zoom", touchstarted)
+        .on("touchmove.zoom", touchmoved)
+        .on("touchend.zoom touchcancel.zoom", touchended)
+        .style("touch-action", "none")
+        .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+  }
+
+  zoom.transform = function(collection, transform) {
+    var selection$$1 = collection.selection ? collection.selection() : collection;
+    selection$$1.property("__zoom", defaultTransform);
+    if (collection !== selection$$1) {
+      schedule(collection, transform);
+    } else {
+      selection$$1.interrupt().each(function() {
+        gesture(this, arguments)
+            .start()
+            .zoom(null, typeof transform === "function" ? transform.apply(this, arguments) : transform)
+            .end();
+      });
+    }
+  };
+
+  zoom.scaleBy = function(selection$$1, k) {
+    zoom.scaleTo(selection$$1, function() {
+      var k0 = this.__zoom.k,
+          k1 = typeof k === "function" ? k.apply(this, arguments) : k;
+      return k0 * k1;
+    });
+  };
+
+  zoom.scaleTo = function(selection$$1, k) {
+    zoom.transform(selection$$1, function() {
+      var e = extent.apply(this, arguments),
+          t0 = this.__zoom,
+          p0 = centroid(e),
+          p1 = t0.invert(p0),
+          k1 = typeof k === "function" ? k.apply(this, arguments) : k;
+      return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
+    });
+  };
+
+  zoom.translateBy = function(selection$$1, x, y) {
+    zoom.transform(selection$$1, function() {
+      return constrain(this.__zoom.translate(
+        typeof x === "function" ? x.apply(this, arguments) : x,
+        typeof y === "function" ? y.apply(this, arguments) : y
+      ), extent.apply(this, arguments), translateExtent);
+    });
+  };
+
+  zoom.translateTo = function(selection$$1, x, y) {
+    zoom.transform(selection$$1, function() {
+      var e = extent.apply(this, arguments),
+          t = this.__zoom,
+          p = centroid(e);
+      return constrain(identity$9.translate(p[0], p[1]).scale(t.k).translate(
+        typeof x === "function" ? -x.apply(this, arguments) : -x,
+        typeof y === "function" ? -y.apply(this, arguments) : -y
+      ), e, translateExtent);
+    });
+  };
+
+  function scale(transform, k) {
+    k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));
+    return k === transform.k ? transform : new Transform(k, transform.x, transform.y);
+  }
+
+  function translate(transform, p0, p1) {
+    var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;
+    return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);
+  }
+
+  function centroid(extent) {
+    return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];
+  }
+
+  function schedule(transition$$1, transform, center) {
+    transition$$1
+        .on("start.zoom", function() { gesture(this, arguments).start(); })
+        .on("interrupt.zoom end.zoom", function() { gesture(this, arguments).end(); })
+        .tween("zoom", function() {
+          var that = this,
+              args = arguments,
+              g = gesture(that, args),
+              e = extent.apply(that, args),
+              p = center || centroid(e),
+              w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),
+              a = that.__zoom,
+              b = typeof transform === "function" ? transform.apply(that, args) : transform,
+              i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
+          return function(t) {
+            if (t === 1) t = b; // Avoid rounding error on end.
+            else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }
+            g.zoom(null, t);
+          };
+        });
+  }
+
+  function gesture(that, args) {
+    for (var i = 0, n = gestures.length, g; i < n; ++i) {
+      if ((g = gestures[i]).that === that) {
+        return g;
+      }
+    }
+    return new Gesture(that, args);
+  }
+
+  function Gesture(that, args) {
+    this.that = that;
+    this.args = args;
+    this.index = -1;
+    this.active = 0;
+    this.extent = extent.apply(that, args);
+  }
+
+  Gesture.prototype = {
+    start: function() {
+      if (++this.active === 1) {
+        this.index = gestures.push(this) - 1;
+        this.emit("start");
+      }
+      return this;
+    },
+    zoom: function(key, transform) {
+      if (this.mouse && key !== "mouse") this.mouse[1] = transform.invert(this.mouse[0]);
+      if (this.touch0 && key !== "touch") this.touch0[1] = transform.invert(this.touch0[0]);
+      if (this.touch1 && key !== "touch") this.touch1[1] = transform.invert(this.touch1[0]);
+      this.that.__zoom = transform;
+      this.emit("zoom");
+      return this;
+    },
+    end: function() {
+      if (--this.active === 0) {
+        gestures.splice(this.index, 1);
+        this.index = -1;
+        this.emit("end");
+      }
+      return this;
+    },
+    emit: function(type) {
+      customEvent(new ZoomEvent(zoom, type, this.that.__zoom), listeners.apply, listeners, [type, this.that, this.args]);
+    }
+  };
+
+  function wheeled() {
+    if (!filter.apply(this, arguments)) return;
+    var g = gesture(this, arguments),
+        t = this.__zoom,
+        k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),
+        p = mouse(this);
+
+    // If the mouse is in the same location as before, reuse it.
+    // If there were recent wheel events, reset the wheel idle timeout.
+    if (g.wheel) {
+      if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {
+        g.mouse[1] = t.invert(g.mouse[0] = p);
+      }
+      clearTimeout(g.wheel);
+    }
+
+    // If this wheel event won’t trigger a transform change, ignore it.
+    else if (t.k === k) return;
+
+    // Otherwise, capture the mouse point and location at the start.
+    else {
+      g.mouse = [p, t.invert(p)];
+      interrupt(this);
+      g.start();
+    }
+
+    noevent$2();
+    g.wheel = setTimeout(wheelidled, wheelDelay);
+    g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));
+
+    function wheelidled() {
+      g.wheel = null;
+      g.end();
+    }
+  }
+
+  function mousedowned() {
+    if (touchending || !filter.apply(this, arguments)) return;
+    var g = gesture(this, arguments),
+        v = select(exports.event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true),
+        p = mouse(this),
+        x0 = exports.event.clientX,
+        y0 = exports.event.clientY;
+
+    dragDisable(exports.event.view);
+    nopropagation$2();
+    g.mouse = [p, this.__zoom.invert(p)];
+    interrupt(this);
+    g.start();
+
+    function mousemoved() {
+      noevent$2();
+      if (!g.moved) {
+        var dx = exports.event.clientX - x0, dy = exports.event.clientY - y0;
+        g.moved = dx * dx + dy * dy > clickDistance2;
+      }
+      g.zoom("mouse", constrain(translate(g.that.__zoom, g.mouse[0] = mouse(g.that), g.mouse[1]), g.extent, translateExtent));
+    }
+
+    function mouseupped() {
+      v.on("mousemove.zoom mouseup.zoom", null);
+      yesdrag(exports.event.view, g.moved);
+      noevent$2();
+      g.end();
+    }
+  }
+
+  function dblclicked() {
+    if (!filter.apply(this, arguments)) return;
+    var t0 = this.__zoom,
+        p0 = mouse(this),
+        p1 = t0.invert(p0),
+        k1 = t0.k * (exports.event.shiftKey ? 0.5 : 2),
+        t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, arguments), translateExtent);
+
+    noevent$2();
+    if (duration > 0) select(this).transition().duration(duration).call(schedule, t1, p0);
+    else select(this).call(zoom.transform, t1);
+  }
+
+  function touchstarted() {
+    if (!filter.apply(this, arguments)) return;
+    var g = gesture(this, arguments),
+        touches$$1 = exports.event.changedTouches,
+        started,
+        n = touches$$1.length, i, t, p;
+
+    nopropagation$2();
+    for (i = 0; i < n; ++i) {
+      t = touches$$1[i], p = touch(this, touches$$1, t.identifier);
+      p = [p, this.__zoom.invert(p), t.identifier];
+      if (!g.touch0) g.touch0 = p, started = true;
+      else if (!g.touch1) g.touch1 = p;
+    }
+
+    // If this is a dbltap, reroute to the (optional) dblclick.zoom handler.
+    if (touchstarting) {
+      touchstarting = clearTimeout(touchstarting);
+      if (!g.touch1) {
+        g.end();
+        p = select(this).on("dblclick.zoom");
+        if (p) p.apply(this, arguments);
+        return;
+      }
+    }
+
+    if (started) {
+      touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay);
+      interrupt(this);
+      g.start();
+    }
+  }
+
+  function touchmoved() {
+    var g = gesture(this, arguments),
+        touches$$1 = exports.event.changedTouches,
+        n = touches$$1.length, i, t, p, l;
+
+    noevent$2();
+    if (touchstarting) touchstarting = clearTimeout(touchstarting);
+    for (i = 0; i < n; ++i) {
+      t = touches$$1[i], p = touch(this, touches$$1, t.identifier);
+      if (g.touch0 && g.touch0[2] === t.identifier) g.touch0[0] = p;
+      else if (g.touch1 && g.touch1[2] === t.identifier) g.touch1[0] = p;
+    }
+    t = g.that.__zoom;
+    if (g.touch1) {
+      var p0 = g.touch0[0], l0 = g.touch0[1],
+          p1 = g.touch1[0], l1 = g.touch1[1],
+          dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,
+          dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;
+      t = scale(t, Math.sqrt(dp / dl));
+      p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
+      l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];
+    }
+    else if (g.touch0) p = g.touch0[0], l = g.touch0[1];
+    else return;
+    g.zoom("touch", constrain(translate(t, p, l), g.extent, translateExtent));
+  }
+
+  function touchended() {
+    var g = gesture(this, arguments),
+        touches$$1 = exports.event.changedTouches,
+        n = touches$$1.length, i, t;
+
+    nopropagation$2();
+    if (touchending) clearTimeout(touchending);
+    touchending = setTimeout(function() { touchending = null; }, touchDelay);
+    for (i = 0; i < n; ++i) {
+      t = touches$$1[i];
+      if (g.touch0 && g.touch0[2] === t.identifier) delete g.touch0;
+      else if (g.touch1 && g.touch1[2] === t.identifier) delete g.touch1;
+    }
+    if (g.touch1 && !g.touch0) g.touch0 = g.touch1, delete g.touch1;
+    if (g.touch0) g.touch0[1] = this.__zoom.invert(g.touch0[0]);
+    else g.end();
+  }
+
+  zoom.wheelDelta = function(_) {
+    return arguments.length ? (wheelDelta = typeof _ === "function" ? _ : constant$d(+_), zoom) : wheelDelta;
+  };
+
+  zoom.filter = function(_) {
+    return arguments.length ? (filter = typeof _ === "function" ? _ : constant$d(!!_), zoom) : filter;
+  };
+
+  zoom.touchable = function(_) {
+    return arguments.length ? (touchable = typeof _ === "function" ? _ : constant$d(!!_), zoom) : touchable;
+  };
+
+  zoom.extent = function(_) {
+    return arguments.length ? (extent = typeof _ === "function" ? _ : constant$d([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
+  };
+
+  zoom.scaleExtent = function(_) {
+    return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
+  };
+
+  zoom.translateExtent = function(_) {
+    return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];
+  };
+
+  zoom.constrain = function(_) {
+    return arguments.length ? (constrain = _, zoom) : constrain;
+  };
+
+  zoom.duration = function(_) {
+    return arguments.length ? (duration = +_, zoom) : duration;
+  };
+
+  zoom.interpolate = function(_) {
+    return arguments.length ? (interpolate = _, zoom) : interpolate;
+  };
+
+  zoom.on = function() {
+    var value = listeners.on.apply(listeners, arguments);
+    return value === listeners ? zoom : value;
+  };
+
+  zoom.clickDistance = function(_) {
+    return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
+  };
+
+  return zoom;
+}
+
+exports.version = version;
+exports.bisect = bisectRight;
+exports.bisectRight = bisectRight;
+exports.bisectLeft = bisectLeft;
+exports.ascending = ascending;
+exports.bisector = bisector;
+exports.cross = cross;
+exports.descending = descending;
+exports.deviation = deviation;
+exports.extent = extent;
+exports.histogram = histogram;
+exports.thresholdFreedmanDiaconis = freedmanDiaconis;
+exports.thresholdScott = scott;
+exports.thresholdSturges = thresholdSturges;
+exports.max = max;
+exports.mean = mean;
+exports.median = median;
+exports.merge = merge;
+exports.min = min;
+exports.pairs = pairs;
+exports.permute = permute;
+exports.quantile = threshold;
+exports.range = sequence;
+exports.scan = scan;
+exports.shuffle = shuffle;
+exports.sum = sum;
+exports.ticks = ticks;
+exports.tickIncrement = tickIncrement;
+exports.tickStep = tickStep;
+exports.transpose = transpose;
+exports.variance = variance;
+exports.zip = zip;
+exports.axisTop = axisTop;
+exports.axisRight = axisRight;
+exports.axisBottom = axisBottom;
+exports.axisLeft = axisLeft;
+exports.brush = brush;
+exports.brushX = brushX;
+exports.brushY = brushY;
+exports.brushSelection = brushSelection;
+exports.chord = chord;
+exports.ribbon = ribbon;
+exports.nest = nest;
+exports.set = set$2;
+exports.map = map$1;
+exports.keys = keys;
+exports.values = values;
+exports.entries = entries;
+exports.color = color;
+exports.rgb = rgb;
+exports.hsl = hsl;
+exports.lab = lab;
+exports.hcl = hcl;
+exports.lch = lch;
+exports.gray = gray;
+exports.cubehelix = cubehelix;
+exports.contours = contours;
+exports.contourDensity = density;
+exports.dispatch = dispatch;
+exports.drag = drag;
+exports.dragDisable = dragDisable;
+exports.dragEnable = yesdrag;
+exports.dsvFormat = dsvFormat;
+exports.csvParse = csvParse;
+exports.csvParseRows = csvParseRows;
+exports.csvFormat = csvFormat;
+exports.csvFormatBody = csvFormatBody;
+exports.csvFormatRows = csvFormatRows;
+exports.tsvParse = tsvParse;
+exports.tsvParseRows = tsvParseRows;
+exports.tsvFormat = tsvFormat;
+exports.tsvFormatBody = tsvFormatBody;
+exports.tsvFormatRows = tsvFormatRows;
+exports.autoType = autoType;
+exports.easeLinear = linear$1;
+exports.easeQuad = quadInOut;
+exports.easeQuadIn = quadIn;
+exports.easeQuadOut = quadOut;
+exports.easeQuadInOut = quadInOut;
+exports.easeCubic = cubicInOut;
+exports.easeCubicIn = cubicIn;
+exports.easeCubicOut = cubicOut;
+exports.easeCubicInOut = cubicInOut;
+exports.easePoly = polyInOut;
+exports.easePolyIn = polyIn;
+exports.easePolyOut = polyOut;
+exports.easePolyInOut = polyInOut;
+exports.easeSin = sinInOut;
+exports.easeSinIn = sinIn;
+exports.easeSinOut = sinOut;
+exports.easeSinInOut = sinInOut;
+exports.easeExp = expInOut;
+exports.easeExpIn = expIn;
+exports.easeExpOut = expOut;
+exports.easeExpInOut = expInOut;
+exports.easeCircle = circleInOut;
+exports.easeCircleIn = circleIn;
+exports.easeCircleOut = circleOut;
+exports.easeCircleInOut = circleInOut;
+exports.easeBounce = bounceOut;
+exports.easeBounceIn = bounceIn;
+exports.easeBounceOut = bounceOut;
+exports.easeBounceInOut = bounceInOut;
+exports.easeBack = backInOut;
+exports.easeBackIn = backIn;
+exports.easeBackOut = backOut;
+exports.easeBackInOut = backInOut;
+exports.easeElastic = elasticOut;
+exports.easeElasticIn = elasticIn;
+exports.easeElasticOut = elasticOut;
+exports.easeElasticInOut = elasticInOut;
+exports.blob = blob;
+exports.buffer = buffer;
+exports.dsv = dsv;
+exports.csv = csv$1;
+exports.tsv = tsv$1;
+exports.image = image;
+exports.json = json;
+exports.text = text;
+exports.xml = xml;
+exports.html = html;
+exports.svg = svg;
+exports.forceCenter = center$1;
+exports.forceCollide = collide;
+exports.forceLink = link;
+exports.forceManyBody = manyBody;
+exports.forceRadial = radial;
+exports.forceSimulation = simulation;
+exports.forceX = x$2;
+exports.forceY = y$2;
+exports.formatDefaultLocale = defaultLocale;
+exports.formatLocale = formatLocale;
+exports.formatSpecifier = formatSpecifier;
+exports.precisionFixed = precisionFixed;
+exports.precisionPrefix = precisionPrefix;
+exports.precisionRound = precisionRound;
+exports.geoArea = area$1;
+exports.geoBounds = bounds;
+exports.geoCentroid = centroid;
+exports.geoCircle = circle;
+exports.geoClipAntimeridian = clipAntimeridian;
+exports.geoClipCircle = clipCircle;
+exports.geoClipExtent = extent$1;
+exports.geoClipRectangle = clipRectangle;
+exports.geoContains = contains$1;
+exports.geoDistance = distance;
+exports.geoGraticule = graticule;
+exports.geoGraticule10 = graticule10;
+exports.geoInterpolate = interpolate$1;
+exports.geoLength = length$1;
+exports.geoPath = index$1;
+exports.geoAlbers = albers;
+exports.geoAlbersUsa = albersUsa;
+exports.geoAzimuthalEqualArea = azimuthalEqualArea;
+exports.geoAzimuthalEqualAreaRaw = azimuthalEqualAreaRaw;
+exports.geoAzimuthalEquidistant = azimuthalEquidistant;
+exports.geoAzimuthalEquidistantRaw = azimuthalEquidistantRaw;
+exports.geoConicConformal = conicConformal;
+exports.geoConicConformalRaw = conicConformalRaw;
+exports.geoConicEqualArea = conicEqualArea;
+exports.geoConicEqualAreaRaw = conicEqualAreaRaw;
+exports.geoConicEquidistant = conicEquidistant;
+exports.geoConicEquidistantRaw = conicEquidistantRaw;
+exports.geoEqualEarth = equalEarth;
+exports.geoEqualEarthRaw = equalEarthRaw;
+exports.geoEquirectangular = equirectangular;
+exports.geoEquirectangularRaw = equirectangularRaw;
+exports.geoGnomonic = gnomonic;
+exports.geoGnomonicRaw = gnomonicRaw;
+exports.geoIdentity = identity$5;
+exports.geoProjection = projection;
+exports.geoProjectionMutator = projectionMutator;
+exports.geoMercator = mercator;
+exports.geoMercatorRaw = mercatorRaw;
+exports.geoNaturalEarth1 = naturalEarth1;
+exports.geoNaturalEarth1Raw = naturalEarth1Raw;
+exports.geoOrthographic = orthographic;
+exports.geoOrthographicRaw = orthographicRaw;
+exports.geoStereographic = stereographic;
+exports.geoStereographicRaw = stereographicRaw;
+exports.geoTransverseMercator = transverseMercator;
+exports.geoTransverseMercatorRaw = transverseMercatorRaw;
+exports.geoRotation = rotation;
+exports.geoStream = geoStream;
+exports.geoTransform = transform;
+exports.cluster = cluster;
+exports.hierarchy = hierarchy;
+exports.pack = index$2;
+exports.packSiblings = siblings;
+exports.packEnclose = enclose;
+exports.partition = partition;
+exports.stratify = stratify;
+exports.tree = tree;
+exports.treemap = index$3;
+exports.treemapBinary = binary;
+exports.treemapDice = treemapDice;
+exports.treemapSlice = treemapSlice;
+exports.treemapSliceDice = sliceDice;
+exports.treemapSquarify = squarify;
+exports.treemapResquarify = resquarify;
+exports.interpolate = interpolateValue;
+exports.interpolateArray = array$1;
+exports.interpolateBasis = basis$1;
+exports.interpolateBasisClosed = basisClosed;
+exports.interpolateDate = date;
+exports.interpolateDiscrete = discrete;
+exports.interpolateHue = hue$1;
+exports.interpolateNumber = interpolateNumber;
+exports.interpolateObject = object;
+exports.interpolateRound = interpolateRound;
+exports.interpolateString = interpolateString;
+exports.interpolateTransformCss = interpolateTransformCss;
+exports.interpolateTransformSvg = interpolateTransformSvg;
+exports.interpolateZoom = interpolateZoom;
+exports.interpolateRgb = interpolateRgb;
+exports.interpolateRgbBasis = rgbBasis;
+exports.interpolateRgbBasisClosed = rgbBasisClosed;
+exports.interpolateHsl = hsl$2;
+exports.interpolateHslLong = hslLong;
+exports.interpolateLab = lab$1;
+exports.interpolateHcl = hcl$2;
+exports.interpolateHclLong = hclLong;
+exports.interpolateCubehelix = cubehelix$2;
+exports.interpolateCubehelixLong = cubehelixLong;
+exports.piecewise = piecewise;
+exports.quantize = quantize;
+exports.path = path;
+exports.polygonArea = area$2;
+exports.polygonCentroid = centroid$1;
+exports.polygonHull = hull;
+exports.polygonContains = contains$2;
+exports.polygonLength = length$2;
+exports.quadtree = quadtree;
+exports.randomUniform = uniform;
+exports.randomNormal = normal;
+exports.randomLogNormal = logNormal;
+exports.randomBates = bates;
+exports.randomIrwinHall = irwinHall;
+exports.randomExponential = exponential$1;
+exports.scaleBand = band;
+exports.scalePoint = point$1;
+exports.scaleIdentity = identity$7;
+exports.scaleLinear = linear$2;
+exports.scaleLog = log$1;
+exports.scaleSymlog = symlog;
+exports.scaleOrdinal = ordinal;
+exports.scaleImplicit = implicit;
+exports.scalePow = pow$1;
+exports.scaleSqrt = sqrt$1;
+exports.scaleQuantile = quantile$$1;
+exports.scaleQuantize = quantize$1;
+exports.scaleThreshold = threshold$1;
+exports.scaleTime = time;
+exports.scaleUtc = utcTime;
+exports.scaleSequential = sequential;
+exports.scaleSequentialLog = sequentialLog;
+exports.scaleSequentialPow = sequentialPow;
+exports.scaleSequentialSqrt = sequentialSqrt;
+exports.scaleSequentialSymlog = sequentialSymlog;
+exports.scaleSequentialQuantile = sequentialQuantile;
+exports.scaleDiverging = diverging;
+exports.scaleDivergingLog = divergingLog;
+exports.scaleDivergingPow = divergingPow;
+exports.scaleDivergingSqrt = divergingSqrt;
+exports.scaleDivergingSymlog = divergingSymlog;
+exports.tickFormat = tickFormat;
+exports.schemeCategory10 = category10;
+exports.schemeAccent = Accent;
+exports.schemeDark2 = Dark2;
+exports.schemePaired = Paired;
+exports.schemePastel1 = Pastel1;
+exports.schemePastel2 = Pastel2;
+exports.schemeSet1 = Set1;
+exports.schemeSet2 = Set2;
+exports.schemeSet3 = Set3;
+exports.interpolateBrBG = BrBG;
+exports.schemeBrBG = scheme;
+exports.interpolatePRGn = PRGn;
+exports.schemePRGn = scheme$1;
+exports.interpolatePiYG = PiYG;
+exports.schemePiYG = scheme$2;
+exports.interpolatePuOr = PuOr;
+exports.schemePuOr = scheme$3;
+exports.interpolateRdBu = RdBu;
+exports.schemeRdBu = scheme$4;
+exports.interpolateRdGy = RdGy;
+exports.schemeRdGy = scheme$5;
+exports.interpolateRdYlBu = RdYlBu;
+exports.schemeRdYlBu = scheme$6;
+exports.interpolateRdYlGn = RdYlGn;
+exports.schemeRdYlGn = scheme$7;
+exports.interpolateSpectral = Spectral;
+exports.schemeSpectral = scheme$8;
+exports.interpolateBuGn = BuGn;
+exports.schemeBuGn = scheme$9;
+exports.interpolateBuPu = BuPu;
+exports.schemeBuPu = scheme$a;
+exports.interpolateGnBu = GnBu;
+exports.schemeGnBu = scheme$b;
+exports.interpolateOrRd = OrRd;
+exports.schemeOrRd = scheme$c;
+exports.interpolatePuBuGn = PuBuGn;
+exports.schemePuBuGn = scheme$d;
+exports.interpolatePuBu = PuBu;
+exports.schemePuBu = scheme$e;
+exports.interpolatePuRd = PuRd;
+exports.schemePuRd = scheme$f;
+exports.interpolateRdPu = RdPu;
+exports.schemeRdPu = scheme$g;
+exports.interpolateYlGnBu = YlGnBu;
+exports.schemeYlGnBu = scheme$h;
+exports.interpolateYlGn = YlGn;
+exports.schemeYlGn = scheme$i;
+exports.interpolateYlOrBr = YlOrBr;
+exports.schemeYlOrBr = scheme$j;
+exports.interpolateYlOrRd = YlOrRd;
+exports.schemeYlOrRd = scheme$k;
+exports.interpolateBlues = Blues;
+exports.schemeBlues = scheme$l;
+exports.interpolateGreens = Greens;
+exports.schemeGreens = scheme$m;
+exports.interpolateGreys = Greys;
+exports.schemeGreys = scheme$n;
+exports.interpolatePurples = Purples;
+exports.schemePurples = scheme$o;
+exports.interpolateReds = Reds;
+exports.schemeReds = scheme$p;
+exports.interpolateOranges = Oranges;
+exports.schemeOranges = scheme$q;
+exports.interpolateCubehelixDefault = cubehelix$3;
+exports.interpolateRainbow = rainbow;
+exports.interpolateWarm = warm;
+exports.interpolateCool = cool;
+exports.interpolateSinebow = sinebow;
+exports.interpolateViridis = viridis;
+exports.interpolateMagma = magma;
+exports.interpolateInferno = inferno;
+exports.interpolatePlasma = plasma;
+exports.create = create;
+exports.creator = creator;
+exports.local = local;
+exports.matcher = matcher;
+exports.mouse = mouse;
+exports.namespace = namespace;
+exports.namespaces = namespaces;
+exports.clientPoint = point;
+exports.select = select;
+exports.selectAll = selectAll;
+exports.selection = selection;
+exports.selector = selector;
+exports.selectorAll = selectorAll;
+exports.style = styleValue;
+exports.touch = touch;
+exports.touches = touches;
+exports.window = defaultView;
+exports.customEvent = customEvent;
+exports.arc = arc;
+exports.area = area$3;
+exports.line = line;
+exports.pie = pie;
+exports.areaRadial = areaRadial;
+exports.radialArea = areaRadial;
+exports.lineRadial = lineRadial$1;
+exports.radialLine = lineRadial$1;
+exports.pointRadial = pointRadial;
+exports.linkHorizontal = linkHorizontal;
+exports.linkVertical = linkVertical;
+exports.linkRadial = linkRadial;
+exports.symbol = symbol;
+exports.symbols = symbols;
+exports.symbolCircle = circle$2;
+exports.symbolCross = cross$2;
+exports.symbolDiamond = diamond;
+exports.symbolSquare = square;
+exports.symbolStar = star;
+exports.symbolTriangle = triangle;
+exports.symbolWye = wye;
+exports.curveBasisClosed = basisClosed$1;
+exports.curveBasisOpen = basisOpen;
+exports.curveBasis = basis$2;
+exports.curveBundle = bundle;
+exports.curveCardinalClosed = cardinalClosed;
+exports.curveCardinalOpen = cardinalOpen;
+exports.curveCardinal = cardinal;
+exports.curveCatmullRomClosed = catmullRomClosed;
+exports.curveCatmullRomOpen = catmullRomOpen;
+exports.curveCatmullRom = catmullRom;
+exports.curveLinearClosed = linearClosed;
+exports.curveLinear = curveLinear;
+exports.curveMonotoneX = monotoneX;
+exports.curveMonotoneY = monotoneY;
+exports.curveNatural = natural;
+exports.curveStep = step;
+exports.curveStepAfter = stepAfter;
+exports.curveStepBefore = stepBefore;
+exports.stack = stack;
+exports.stackOffsetExpand = expand;
+exports.stackOffsetDiverging = diverging$1;
+exports.stackOffsetNone = none$1;
+exports.stackOffsetSilhouette = silhouette;
+exports.stackOffsetWiggle = wiggle;
+exports.stackOrderAppearance = appearance;
+exports.stackOrderAscending = ascending$3;
+exports.stackOrderDescending = descending$2;
+exports.stackOrderInsideOut = insideOut;
+exports.stackOrderNone = none$2;
+exports.stackOrderReverse = reverse;
+exports.timeInterval = newInterval;
+exports.timeMillisecond = millisecond;
+exports.timeMilliseconds = milliseconds;
+exports.utcMillisecond = millisecond;
+exports.utcMilliseconds = milliseconds;
+exports.timeSecond = second;
+exports.timeSeconds = seconds;
+exports.utcSecond = second;
+exports.utcSeconds = seconds;
+exports.timeMinute = minute;
+exports.timeMinutes = minutes;
+exports.timeHour = hour;
+exports.timeHours = hours;
+exports.timeDay = day;
+exports.timeDays = days;
+exports.timeWeek = sunday;
+exports.timeWeeks = sundays;
+exports.timeSunday = sunday;
+exports.timeSundays = sundays;
+exports.timeMonday = monday;
+exports.timeMondays = mondays;
+exports.timeTuesday = tuesday;
+exports.timeTuesdays = tuesdays;
+exports.timeWednesday = wednesday;
+exports.timeWednesdays = wednesdays;
+exports.timeThursday = thursday;
+exports.timeThursdays = thursdays;
+exports.timeFriday = friday;
+exports.timeFridays = fridays;
+exports.timeSaturday = saturday;
+exports.timeSaturdays = saturdays;
+exports.timeMonth = month;
+exports.timeMonths = months;
+exports.timeYear = year;
+exports.timeYears = years;
+exports.utcMinute = utcMinute;
+exports.utcMinutes = utcMinutes;
+exports.utcHour = utcHour;
+exports.utcHours = utcHours;
+exports.utcDay = utcDay;
+exports.utcDays = utcDays;
+exports.utcWeek = utcSunday;
+exports.utcWeeks = utcSundays;
+exports.utcSunday = utcSunday;
+exports.utcSundays = utcSundays;
+exports.utcMonday = utcMonday;
+exports.utcMondays = utcMondays;
+exports.utcTuesday = utcTuesday;
+exports.utcTuesdays = utcTuesdays;
+exports.utcWednesday = utcWednesday;
+exports.utcWednesdays = utcWednesdays;
+exports.utcThursday = utcThursday;
+exports.utcThursdays = utcThursdays;
+exports.utcFriday = utcFriday;
+exports.utcFridays = utcFridays;
+exports.utcSaturday = utcSaturday;
+exports.utcSaturdays = utcSaturdays;
+exports.utcMonth = utcMonth;
+exports.utcMonths = utcMonths;
+exports.utcYear = utcYear;
+exports.utcYears = utcYears;
+exports.timeFormatDefaultLocale = defaultLocale$1;
+exports.timeFormatLocale = formatLocale$1;
+exports.isoFormat = formatIso;
+exports.isoParse = parseIso;
+exports.now = now;
+exports.timer = timer;
+exports.timerFlush = timerFlush;
+exports.timeout = timeout$1;
+exports.interval = interval$1;
+exports.transition = transition;
+exports.active = active;
+exports.interrupt = interrupt;
+exports.voronoi = voronoi;
+exports.zoom = zoom;
+exports.zoomTransform = transform$1;
+exports.zoomIdentity = identity$9;
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
diff --git a/system/javascript/osapjs/client/libs/d3.v6.js b/system/javascript/osapjs/client/libs/d3.v6.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed68404bf3cbfbb5c974f206bb8e6214c4bb93a6
--- /dev/null
+++ b/system/javascript/osapjs/client/libs/d3.v6.js
@@ -0,0 +1,19709 @@
+// https://d3js.org v6.7.0 Copyright 2021 Mike Bostock
+(function (global, factory) {
+    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+    typeof define === 'function' && define.amd ? define(['exports'], factory) :
+    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}));
+    }(this, (function (exports) { 'use strict';
+    
+    var version = "6.7.0";
+    
+    function ascending$3(a, b) {
+      return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+    }
+    
+    function bisector(f) {
+      let delta = f;
+      let compare = f;
+    
+      if (f.length === 1) {
+        delta = (d, x) => f(d) - x;
+        compare = ascendingComparator(f);
+      }
+    
+      function left(a, x, lo, hi) {
+        if (lo == null) lo = 0;
+        if (hi == null) hi = a.length;
+        while (lo < hi) {
+          const mid = (lo + hi) >>> 1;
+          if (compare(a[mid], x) < 0) lo = mid + 1;
+          else hi = mid;
+        }
+        return lo;
+      }
+    
+      function right(a, x, lo, hi) {
+        if (lo == null) lo = 0;
+        if (hi == null) hi = a.length;
+        while (lo < hi) {
+          const mid = (lo + hi) >>> 1;
+          if (compare(a[mid], x) > 0) hi = mid;
+          else lo = mid + 1;
+        }
+        return lo;
+      }
+    
+      function center(a, x, lo, hi) {
+        if (lo == null) lo = 0;
+        if (hi == null) hi = a.length;
+        const i = left(a, x, lo, hi - 1);
+        return i > lo && delta(a[i - 1], x) > -delta(a[i], x) ? i - 1 : i;
+      }
+    
+      return {left, center, right};
+    }
+    
+    function ascendingComparator(f) {
+      return (d, x) => ascending$3(f(d), x);
+    }
+    
+    function number$3(x) {
+      return x === null ? NaN : +x;
+    }
+    
+    function* numbers(values, valueof) {
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value != null && (value = +value) >= value) {
+            yield value;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
+            yield value;
+          }
+        }
+      }
+    }
+    
+    const ascendingBisect = bisector(ascending$3);
+    const bisectRight = ascendingBisect.right;
+    const bisectLeft = ascendingBisect.left;
+    const bisectCenter = bisector(number$3).center;
+    
+    function count$1(values, valueof) {
+      let count = 0;
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value != null && (value = +value) >= value) {
+            ++count;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
+            ++count;
+          }
+        }
+      }
+      return count;
+    }
+    
+    function length$3(array) {
+      return array.length | 0;
+    }
+    
+    function empty$2(length) {
+      return !(length > 0);
+    }
+    
+    function arrayify(values) {
+      return typeof values !== "object" || "length" in values ? values : Array.from(values);
+    }
+    
+    function reducer(reduce) {
+      return values => reduce(...values);
+    }
+    
+    function cross$2(...values) {
+      const reduce = typeof values[values.length - 1] === "function" && reducer(values.pop());
+      values = values.map(arrayify);
+      const lengths = values.map(length$3);
+      const j = values.length - 1;
+      const index = new Array(j + 1).fill(0);
+      const product = [];
+      if (j < 0 || lengths.some(empty$2)) return product;
+      while (true) {
+        product.push(index.map((j, i) => values[i][j]));
+        let i = j;
+        while (++index[i] === lengths[i]) {
+          if (i === 0) return reduce ? product.map(reduce) : product;
+          index[i--] = 0;
+        }
+      }
+    }
+    
+    function cumsum(values, valueof) {
+      var sum = 0, index = 0;
+      return Float64Array.from(values, valueof === undefined
+        ? v => (sum += +v || 0)
+        : v => (sum += +valueof(v, index++, values) || 0));
+    }
+    
+    function descending$2(a, b) {
+      return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+    }
+    
+    function variance(values, valueof) {
+      let count = 0;
+      let delta;
+      let mean = 0;
+      let sum = 0;
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value != null && (value = +value) >= value) {
+            delta = value - mean;
+            mean += delta / ++count;
+            sum += delta * (value - mean);
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
+            delta = value - mean;
+            mean += delta / ++count;
+            sum += delta * (value - mean);
+          }
+        }
+      }
+      if (count > 1) return sum / (count - 1);
+    }
+    
+    function deviation(values, valueof) {
+      const v = variance(values, valueof);
+      return v ? Math.sqrt(v) : v;
+    }
+    
+    function extent$1(values, valueof) {
+      let min;
+      let max;
+      if (valueof === undefined) {
+        for (const value of values) {
+          if (value != null) {
+            if (min === undefined) {
+              if (value >= value) min = max = value;
+            } else {
+              if (min > value) min = value;
+              if (max < value) max = value;
+            }
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null) {
+            if (min === undefined) {
+              if (value >= value) min = max = value;
+            } else {
+              if (min > value) min = value;
+              if (max < value) max = value;
+            }
+          }
+        }
+      }
+      return [min, max];
+    }
+    
+    // https://github.com/python/cpython/blob/a74eea238f5baba15797e2e8b570d153bc8690a7/Modules/mathmodule.c#L1423
+    class Adder {
+      constructor() {
+        this._partials = new Float64Array(32);
+        this._n = 0;
+      }
+      add(x) {
+        const p = this._partials;
+        let i = 0;
+        for (let j = 0; j < this._n && j < 32; j++) {
+          const y = p[j],
+            hi = x + y,
+            lo = Math.abs(x) < Math.abs(y) ? x - (hi - y) : y - (hi - x);
+          if (lo) p[i++] = lo;
+          x = hi;
+        }
+        p[i] = x;
+        this._n = i + 1;
+        return this;
+      }
+      valueOf() {
+        const p = this._partials;
+        let n = this._n, x, y, lo, hi = 0;
+        if (n > 0) {
+          hi = p[--n];
+          while (n > 0) {
+            x = hi;
+            y = p[--n];
+            hi = x + y;
+            lo = y - (hi - x);
+            if (lo) break;
+          }
+          if (n > 0 && ((lo < 0 && p[n - 1] < 0) || (lo > 0 && p[n - 1] > 0))) {
+            y = lo * 2;
+            x = hi + y;
+            if (y == x - hi) hi = x;
+          }
+        }
+        return hi;
+      }
+    }
+    
+    function fsum(values, valueof) {
+      const adder = new Adder();
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value = +value) {
+            adder.add(value);
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if (value = +valueof(value, ++index, values)) {
+            adder.add(value);
+          }
+        }
+      }
+      return +adder;
+    }
+    
+    function fcumsum(values, valueof) {
+      const adder = new Adder();
+      let index = -1;
+      return Float64Array.from(values, valueof === undefined
+          ? v => adder.add(+v || 0)
+          : v => adder.add(+valueof(v, ++index, values) || 0)
+      );
+    }
+    
+    class InternMap extends Map {
+      constructor(entries, key = keyof) {
+        super();
+        Object.defineProperties(this, {_intern: {value: new Map()}, _key: {value: key}});
+        if (entries != null) for (const [key, value] of entries) this.set(key, value);
+      }
+      get(key) {
+        return super.get(intern_get(this, key));
+      }
+      has(key) {
+        return super.has(intern_get(this, key));
+      }
+      set(key, value) {
+        return super.set(intern_set(this, key), value);
+      }
+      delete(key) {
+        return super.delete(intern_delete(this, key));
+      }
+    }
+    
+    class InternSet extends Set {
+      constructor(values, key = keyof) {
+        super();
+        Object.defineProperties(this, {_intern: {value: new Map()}, _key: {value: key}});
+        if (values != null) for (const value of values) this.add(value);
+      }
+      has(value) {
+        return super.has(intern_get(this, value));
+      }
+      add(value) {
+        return super.add(intern_set(this, value));
+      }
+      delete(value) {
+        return super.delete(intern_delete(this, value));
+      }
+    }
+    
+    function intern_get({_intern, _key}, value) {
+      const key = _key(value);
+      return _intern.has(key) ? _intern.get(key) : value;
+    }
+    
+    function intern_set({_intern, _key}, value) {
+      const key = _key(value);
+      if (_intern.has(key)) return _intern.get(key);
+      _intern.set(key, value);
+      return value;
+    }
+    
+    function intern_delete({_intern, _key}, value) {
+      const key = _key(value);
+      if (_intern.has(key)) {
+        value = _intern.get(value);
+        _intern.delete(key);
+      }
+      return value;
+    }
+    
+    function keyof(value) {
+      return value !== null && typeof value === "object" ? value.valueOf() : value;
+    }
+    
+    function identity$9(x) {
+      return x;
+    }
+    
+    function group(values, ...keys) {
+      return nest(values, identity$9, identity$9, keys);
+    }
+    
+    function groups(values, ...keys) {
+      return nest(values, Array.from, identity$9, keys);
+    }
+    
+    function rollup(values, reduce, ...keys) {
+      return nest(values, identity$9, reduce, keys);
+    }
+    
+    function rollups(values, reduce, ...keys) {
+      return nest(values, Array.from, reduce, keys);
+    }
+    
+    function index$4(values, ...keys) {
+      return nest(values, identity$9, unique, keys);
+    }
+    
+    function indexes(values, ...keys) {
+      return nest(values, Array.from, unique, keys);
+    }
+    
+    function unique(values) {
+      if (values.length !== 1) throw new Error("duplicate key");
+      return values[0];
+    }
+    
+    function nest(values, map, reduce, keys) {
+      return (function regroup(values, i) {
+        if (i >= keys.length) return reduce(values);
+        const groups = new InternMap();
+        const keyof = keys[i++];
+        let index = -1;
+        for (const value of values) {
+          const key = keyof(value, ++index, values);
+          const group = groups.get(key);
+          if (group) group.push(value);
+          else groups.set(key, [value]);
+        }
+        for (const [key, values] of groups) {
+          groups.set(key, regroup(values, i));
+        }
+        return map(groups);
+      })(values, 0);
+    }
+    
+    function permute(source, keys) {
+      return Array.from(keys, key => source[key]);
+    }
+    
+    function sort(values, ...F) {
+      if (typeof values[Symbol.iterator] !== "function") throw new TypeError("values is not iterable");
+      values = Array.from(values);
+      let [f = ascending$3] = F;
+      if (f.length === 1 || F.length > 1) {
+        const index = Uint32Array.from(values, (d, i) => i);
+        if (F.length > 1) {
+          F = F.map(f => values.map(f));
+          index.sort((i, j) => {
+            for (const f of F) {
+              const c = ascending$3(f[i], f[j]);
+              if (c) return c;
+            }
+          });
+        } else {
+          f = values.map(f);
+          index.sort((i, j) => ascending$3(f[i], f[j]));
+        }
+        return permute(values, index);
+      }
+      return values.sort(f);
+    }
+    
+    function groupSort(values, reduce, key) {
+      return (reduce.length === 1
+        ? sort(rollup(values, reduce, key), (([ak, av], [bk, bv]) => ascending$3(av, bv) || ascending$3(ak, bk)))
+        : sort(group(values, key), (([ak, av], [bk, bv]) => reduce(av, bv) || ascending$3(ak, bk))))
+        .map(([key]) => key);
+    }
+    
+    var array$5 = Array.prototype;
+    
+    var slice$4 = array$5.slice;
+    
+    function constant$b(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    var e10 = Math.sqrt(50),
+        e5 = Math.sqrt(10),
+        e2 = Math.sqrt(2);
+    
+    function ticks(start, stop, count) {
+      var reverse,
+          i = -1,
+          n,
+          ticks,
+          step;
+    
+      stop = +stop, start = +start, count = +count;
+      if (start === stop && count > 0) return [start];
+      if (reverse = stop < start) n = start, start = stop, stop = n;
+      if ((step = tickIncrement(start, stop, count)) === 0 || !isFinite(step)) return [];
+    
+      if (step > 0) {
+        let r0 = Math.round(start / step), r1 = Math.round(stop / step);
+        if (r0 * step < start) ++r0;
+        if (r1 * step > stop) --r1;
+        ticks = new Array(n = r1 - r0 + 1);
+        while (++i < n) ticks[i] = (r0 + i) * step;
+      } else {
+        step = -step;
+        let r0 = Math.round(start * step), r1 = Math.round(stop * step);
+        if (r0 / step < start) ++r0;
+        if (r1 / step > stop) --r1;
+        ticks = new Array(n = r1 - r0 + 1);
+        while (++i < n) ticks[i] = (r0 + i) / step;
+      }
+    
+      if (reverse) ticks.reverse();
+    
+      return ticks;
+    }
+    
+    function tickIncrement(start, stop, count) {
+      var step = (stop - start) / Math.max(0, count),
+          power = Math.floor(Math.log(step) / Math.LN10),
+          error = step / Math.pow(10, power);
+      return power >= 0
+          ? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(10, power)
+          : -Math.pow(10, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1);
+    }
+    
+    function tickStep(start, stop, count) {
+      var step0 = Math.abs(stop - start) / Math.max(0, count),
+          step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
+          error = step0 / step1;
+      if (error >= e10) step1 *= 10;
+      else if (error >= e5) step1 *= 5;
+      else if (error >= e2) step1 *= 2;
+      return stop < start ? -step1 : step1;
+    }
+    
+    function nice$1(start, stop, count) {
+      let prestep;
+      while (true) {
+        const step = tickIncrement(start, stop, count);
+        if (step === prestep || step === 0 || !isFinite(step)) {
+          return [start, stop];
+        } else if (step > 0) {
+          start = Math.floor(start / step) * step;
+          stop = Math.ceil(stop / step) * step;
+        } else if (step < 0) {
+          start = Math.ceil(start * step) / step;
+          stop = Math.floor(stop * step) / step;
+        }
+        prestep = step;
+      }
+    }
+    
+    function thresholdSturges(values) {
+      return Math.ceil(Math.log(count$1(values)) / Math.LN2) + 1;
+    }
+    
+    function bin() {
+      var value = identity$9,
+          domain = extent$1,
+          threshold = thresholdSturges;
+    
+      function histogram(data) {
+        if (!Array.isArray(data)) data = Array.from(data);
+    
+        var i,
+            n = data.length,
+            x,
+            values = new Array(n);
+    
+        for (i = 0; i < n; ++i) {
+          values[i] = value(data[i], i, data);
+        }
+    
+        var xz = domain(values),
+            x0 = xz[0],
+            x1 = xz[1],
+            tz = threshold(values, x0, x1);
+    
+        // Convert number of thresholds into uniform thresholds, and nice the
+        // default domain accordingly.
+        if (!Array.isArray(tz)) {
+          const max = x1, tn = +tz;
+          if (domain === extent$1) [x0, x1] = nice$1(x0, x1, tn);
+          tz = ticks(x0, x1, tn);
+    
+          // If the last threshold is coincident with the domain’s upper bound, the
+          // last bin will be zero-width. If the default domain is used, and this
+          // last threshold is coincident with the maximum input value, we can
+          // extend the niced upper bound by one tick to ensure uniform bin widths;
+          // otherwise, we simply remove the last threshold. Note that we don’t
+          // coerce values or the domain to numbers, and thus must be careful to
+          // compare order (>=) rather than strict equality (===)!
+          if (tz[tz.length - 1] >= x1) {
+            if (max >= x1 && domain === extent$1) {
+              const step = tickIncrement(x0, x1, tn);
+              if (isFinite(step)) {
+                if (step > 0) {
+                  x1 = (Math.floor(x1 / step) + 1) * step;
+                } else if (step < 0) {
+                  x1 = (Math.ceil(x1 * -step) + 1) / -step;
+                }
+              }
+            } else {
+              tz.pop();
+            }
+          }
+        }
+    
+        // Remove any thresholds outside the domain.
+        var m = tz.length;
+        while (tz[0] <= x0) tz.shift(), --m;
+        while (tz[m - 1] > x1) tz.pop(), --m;
+    
+        var bins = new Array(m + 1),
+            bin;
+    
+        // Initialize bins.
+        for (i = 0; i <= m; ++i) {
+          bin = bins[i] = [];
+          bin.x0 = i > 0 ? tz[i - 1] : x0;
+          bin.x1 = i < m ? tz[i] : x1;
+        }
+    
+        // Assign data to bins by value, ignoring any outside the domain.
+        for (i = 0; i < n; ++i) {
+          x = values[i];
+          if (x0 <= x && x <= x1) {
+            bins[bisectRight(tz, x, 0, m)].push(data[i]);
+          }
+        }
+    
+        return bins;
+      }
+    
+      histogram.value = function(_) {
+        return arguments.length ? (value = typeof _ === "function" ? _ : constant$b(_), histogram) : value;
+      };
+    
+      histogram.domain = function(_) {
+        return arguments.length ? (domain = typeof _ === "function" ? _ : constant$b([_[0], _[1]]), histogram) : domain;
+      };
+    
+      histogram.thresholds = function(_) {
+        return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant$b(slice$4.call(_)) : constant$b(_), histogram) : threshold;
+      };
+    
+      return histogram;
+    }
+    
+    function max$3(values, valueof) {
+      let max;
+      if (valueof === undefined) {
+        for (const value of values) {
+          if (value != null
+              && (max < value || (max === undefined && value >= value))) {
+            max = value;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null
+              && (max < value || (max === undefined && value >= value))) {
+            max = value;
+          }
+        }
+      }
+      return max;
+    }
+    
+    function min$2(values, valueof) {
+      let min;
+      if (valueof === undefined) {
+        for (const value of values) {
+          if (value != null
+              && (min > value || (min === undefined && value >= value))) {
+            min = value;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null
+              && (min > value || (min === undefined && value >= value))) {
+            min = value;
+          }
+        }
+      }
+      return min;
+    }
+    
+    // Based on https://github.com/mourner/quickselect
+    // ISC license, Copyright 2018 Vladimir Agafonkin.
+    function quickselect(array, k, left = 0, right = array.length - 1, compare = ascending$3) {
+      while (right > left) {
+        if (right - left > 600) {
+          const n = right - left + 1;
+          const m = k - left + 1;
+          const z = Math.log(n);
+          const s = 0.5 * Math.exp(2 * z / 3);
+          const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
+          const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
+          const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
+          quickselect(array, k, newLeft, newRight, compare);
+        }
+    
+        const t = array[k];
+        let i = left;
+        let j = right;
+    
+        swap$1(array, left, k);
+        if (compare(array[right], t) > 0) swap$1(array, left, right);
+    
+        while (i < j) {
+          swap$1(array, i, j), ++i, --j;
+          while (compare(array[i], t) < 0) ++i;
+          while (compare(array[j], t) > 0) --j;
+        }
+    
+        if (compare(array[left], t) === 0) swap$1(array, left, j);
+        else ++j, swap$1(array, j, right);
+    
+        if (j <= k) left = j + 1;
+        if (k <= j) right = j - 1;
+      }
+      return array;
+    }
+    
+    function swap$1(array, i, j) {
+      const t = array[i];
+      array[i] = array[j];
+      array[j] = t;
+    }
+    
+    function quantile$1(values, p, valueof) {
+      values = Float64Array.from(numbers(values, valueof));
+      if (!(n = values.length)) return;
+      if ((p = +p) <= 0 || n < 2) return min$2(values);
+      if (p >= 1) return max$3(values);
+      var n,
+          i = (n - 1) * p,
+          i0 = Math.floor(i),
+          value0 = max$3(quickselect(values, i0).subarray(0, i0 + 1)),
+          value1 = min$2(values.subarray(i0 + 1));
+      return value0 + (value1 - value0) * (i - i0);
+    }
+    
+    function quantileSorted(values, p, valueof = number$3) {
+      if (!(n = values.length)) return;
+      if ((p = +p) <= 0 || n < 2) return +valueof(values[0], 0, values);
+      if (p >= 1) return +valueof(values[n - 1], n - 1, values);
+      var n,
+          i = (n - 1) * p,
+          i0 = Math.floor(i),
+          value0 = +valueof(values[i0], i0, values),
+          value1 = +valueof(values[i0 + 1], i0 + 1, values);
+      return value0 + (value1 - value0) * (i - i0);
+    }
+    
+    function freedmanDiaconis(values, min, max) {
+      return Math.ceil((max - min) / (2 * (quantile$1(values, 0.75) - quantile$1(values, 0.25)) * Math.pow(count$1(values), -1 / 3)));
+    }
+    
+    function scott(values, min, max) {
+      return Math.ceil((max - min) / (3.5 * deviation(values) * Math.pow(count$1(values), -1 / 3)));
+    }
+    
+    function maxIndex(values, valueof) {
+      let max;
+      let maxIndex = -1;
+      let index = -1;
+      if (valueof === undefined) {
+        for (const value of values) {
+          ++index;
+          if (value != null
+              && (max < value || (max === undefined && value >= value))) {
+            max = value, maxIndex = index;
+          }
+        }
+      } else {
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null
+              && (max < value || (max === undefined && value >= value))) {
+            max = value, maxIndex = index;
+          }
+        }
+      }
+      return maxIndex;
+    }
+    
+    function mean(values, valueof) {
+      let count = 0;
+      let sum = 0;
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value != null && (value = +value) >= value) {
+            ++count, sum += value;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
+            ++count, sum += value;
+          }
+        }
+      }
+      if (count) return sum / count;
+    }
+    
+    function median(values, valueof) {
+      return quantile$1(values, 0.5, valueof);
+    }
+    
+    function* flatten(arrays) {
+      for (const array of arrays) {
+        yield* array;
+      }
+    }
+    
+    function merge(arrays) {
+      return Array.from(flatten(arrays));
+    }
+    
+    function minIndex(values, valueof) {
+      let min;
+      let minIndex = -1;
+      let index = -1;
+      if (valueof === undefined) {
+        for (const value of values) {
+          ++index;
+          if (value != null
+              && (min > value || (min === undefined && value >= value))) {
+            min = value, minIndex = index;
+          }
+        }
+      } else {
+        for (let value of values) {
+          if ((value = valueof(value, ++index, values)) != null
+              && (min > value || (min === undefined && value >= value))) {
+            min = value, minIndex = index;
+          }
+        }
+      }
+      return minIndex;
+    }
+    
+    function pairs(values, pairof = pair) {
+      const pairs = [];
+      let previous;
+      let first = false;
+      for (const value of values) {
+        if (first) pairs.push(pairof(previous, value));
+        previous = value;
+        first = true;
+      }
+      return pairs;
+    }
+    
+    function pair(a, b) {
+      return [a, b];
+    }
+    
+    function sequence(start, stop, step) {
+      start = +start, stop = +stop, step = (n = arguments.length) < 2 ? (stop = start, start = 0, 1) : n < 3 ? 1 : +step;
+    
+      var i = -1,
+          n = Math.max(0, Math.ceil((stop - start) / step)) | 0,
+          range = new Array(n);
+    
+      while (++i < n) {
+        range[i] = start + i * step;
+      }
+    
+      return range;
+    }
+    
+    function least(values, compare = ascending$3) {
+      let min;
+      let defined = false;
+      if (compare.length === 1) {
+        let minValue;
+        for (const element of values) {
+          const value = compare(element);
+          if (defined
+              ? ascending$3(value, minValue) < 0
+              : ascending$3(value, value) === 0) {
+            min = element;
+            minValue = value;
+            defined = true;
+          }
+        }
+      } else {
+        for (const value of values) {
+          if (defined
+              ? compare(value, min) < 0
+              : compare(value, value) === 0) {
+            min = value;
+            defined = true;
+          }
+        }
+      }
+      return min;
+    }
+    
+    function leastIndex(values, compare = ascending$3) {
+      if (compare.length === 1) return minIndex(values, compare);
+      let minValue;
+      let min = -1;
+      let index = -1;
+      for (const value of values) {
+        ++index;
+        if (min < 0
+            ? compare(value, value) === 0
+            : compare(value, minValue) < 0) {
+          minValue = value;
+          min = index;
+        }
+      }
+      return min;
+    }
+    
+    function greatest(values, compare = ascending$3) {
+      let max;
+      let defined = false;
+      if (compare.length === 1) {
+        let maxValue;
+        for (const element of values) {
+          const value = compare(element);
+          if (defined
+              ? ascending$3(value, maxValue) > 0
+              : ascending$3(value, value) === 0) {
+            max = element;
+            maxValue = value;
+            defined = true;
+          }
+        }
+      } else {
+        for (const value of values) {
+          if (defined
+              ? compare(value, max) > 0
+              : compare(value, value) === 0) {
+            max = value;
+            defined = true;
+          }
+        }
+      }
+      return max;
+    }
+    
+    function greatestIndex(values, compare = ascending$3) {
+      if (compare.length === 1) return maxIndex(values, compare);
+      let maxValue;
+      let max = -1;
+      let index = -1;
+      for (const value of values) {
+        ++index;
+        if (max < 0
+            ? compare(value, value) === 0
+            : compare(value, maxValue) > 0) {
+          maxValue = value;
+          max = index;
+        }
+      }
+      return max;
+    }
+    
+    function scan(values, compare) {
+      const index = leastIndex(values, compare);
+      return index < 0 ? undefined : index;
+    }
+    
+    var shuffle$1 = shuffler(Math.random);
+    
+    function shuffler(random) {
+      return function shuffle(array, i0 = 0, i1 = array.length) {
+        let m = i1 - (i0 = +i0);
+        while (m) {
+          const i = random() * m-- | 0, t = array[m + i0];
+          array[m + i0] = array[i + i0];
+          array[i + i0] = t;
+        }
+        return array;
+      };
+    }
+    
+    function sum$1(values, valueof) {
+      let sum = 0;
+      if (valueof === undefined) {
+        for (let value of values) {
+          if (value = +value) {
+            sum += value;
+          }
+        }
+      } else {
+        let index = -1;
+        for (let value of values) {
+          if (value = +valueof(value, ++index, values)) {
+            sum += value;
+          }
+        }
+      }
+      return sum;
+    }
+    
+    function transpose(matrix) {
+      if (!(n = matrix.length)) return [];
+      for (var i = -1, m = min$2(matrix, length$2), transpose = new Array(m); ++i < m;) {
+        for (var j = -1, n, row = transpose[i] = new Array(n); ++j < n;) {
+          row[j] = matrix[j][i];
+        }
+      }
+      return transpose;
+    }
+    
+    function length$2(d) {
+      return d.length;
+    }
+    
+    function zip() {
+      return transpose(arguments);
+    }
+    
+    function every(values, test) {
+      if (typeof test !== "function") throw new TypeError("test is not a function");
+      let index = -1;
+      for (const value of values) {
+        if (!test(value, ++index, values)) {
+          return false;
+        }
+      }
+      return true;
+    }
+    
+    function some(values, test) {
+      if (typeof test !== "function") throw new TypeError("test is not a function");
+      let index = -1;
+      for (const value of values) {
+        if (test(value, ++index, values)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    
+    function filter$1(values, test) {
+      if (typeof test !== "function") throw new TypeError("test is not a function");
+      const array = [];
+      let index = -1;
+      for (const value of values) {
+        if (test(value, ++index, values)) {
+          array.push(value);
+        }
+      }
+      return array;
+    }
+    
+    function map$1(values, mapper) {
+      if (typeof values[Symbol.iterator] !== "function") throw new TypeError("values is not iterable");
+      if (typeof mapper !== "function") throw new TypeError("mapper is not a function");
+      return Array.from(values, (value, index) => mapper(value, index, values));
+    }
+    
+    function reduce(values, reducer, value) {
+      if (typeof reducer !== "function") throw new TypeError("reducer is not a function");
+      const iterator = values[Symbol.iterator]();
+      let done, next, index = -1;
+      if (arguments.length < 3) {
+        ({done, value} = iterator.next());
+        if (done) return;
+        ++index;
+      }
+      while (({done, value: next} = iterator.next()), !done) {
+        value = reducer(value, next, ++index, values);
+      }
+      return value;
+    }
+    
+    function reverse$1(values) {
+      if (typeof values[Symbol.iterator] !== "function") throw new TypeError("values is not iterable");
+      return Array.from(values).reverse();
+    }
+    
+    function difference(values, ...others) {
+      values = new Set(values);
+      for (const other of others) {
+        for (const value of other) {
+          values.delete(value);
+        }
+      }
+      return values;
+    }
+    
+    function disjoint(values, other) {
+      const iterator = other[Symbol.iterator](), set = new Set();
+      for (const v of values) {
+        if (set.has(v)) return false;
+        let value, done;
+        while (({value, done} = iterator.next())) {
+          if (done) break;
+          if (Object.is(v, value)) return false;
+          set.add(value);
+        }
+      }
+      return true;
+    }
+    
+    function set$2(values) {
+      return values instanceof Set ? values : new Set(values);
+    }
+    
+    function intersection(values, ...others) {
+      values = new Set(values);
+      others = others.map(set$2);
+      out: for (const value of values) {
+        for (const other of others) {
+          if (!other.has(value)) {
+            values.delete(value);
+            continue out;
+          }
+        }
+      }
+      return values;
+    }
+    
+    function superset(values, other) {
+      const iterator = values[Symbol.iterator](), set = new Set();
+      for (const o of other) {
+        if (set.has(o)) continue;
+        let value, done;
+        while (({value, done} = iterator.next())) {
+          if (done) return false;
+          set.add(value);
+          if (Object.is(o, value)) break;
+        }
+      }
+      return true;
+    }
+    
+    function subset(values, other) {
+      return superset(other, values);
+    }
+    
+    function union(...others) {
+      const set = new Set();
+      for (const other of others) {
+        for (const o of other) {
+          set.add(o);
+        }
+      }
+      return set;
+    }
+    
+    var slice$3 = Array.prototype.slice;
+    
+    function identity$8(x) {
+      return x;
+    }
+    
+    var top = 1,
+        right = 2,
+        bottom = 3,
+        left = 4,
+        epsilon$5 = 1e-6;
+    
+    function translateX(x) {
+      return "translate(" + x + ",0)";
+    }
+    
+    function translateY(y) {
+      return "translate(0," + y + ")";
+    }
+    
+    function number$2(scale) {
+      return d => +scale(d);
+    }
+    
+    function center$1(scale, offset) {
+      offset = Math.max(0, scale.bandwidth() - offset * 2) / 2;
+      if (scale.round()) offset = Math.round(offset);
+      return d => +scale(d) + offset;
+    }
+    
+    function entering() {
+      return !this.__axis;
+    }
+    
+    function axis(orient, scale) {
+      var tickArguments = [],
+          tickValues = null,
+          tickFormat = null,
+          tickSizeInner = 6,
+          tickSizeOuter = 6,
+          tickPadding = 3,
+          offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5,
+          k = orient === top || orient === left ? -1 : 1,
+          x = orient === left || orient === right ? "x" : "y",
+          transform = orient === top || orient === bottom ? translateX : translateY;
+    
+      function axis(context) {
+        var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues,
+            format = tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity$8) : tickFormat,
+            spacing = Math.max(tickSizeInner, 0) + tickPadding,
+            range = scale.range(),
+            range0 = +range[0] + offset,
+            range1 = +range[range.length - 1] + offset,
+            position = (scale.bandwidth ? center$1 : number$2)(scale.copy(), offset),
+            selection = context.selection ? context.selection() : context,
+            path = selection.selectAll(".domain").data([null]),
+            tick = selection.selectAll(".tick").data(values, scale).order(),
+            tickExit = tick.exit(),
+            tickEnter = tick.enter().append("g").attr("class", "tick"),
+            line = tick.select("line"),
+            text = tick.select("text");
+    
+        path = path.merge(path.enter().insert("path", ".tick")
+            .attr("class", "domain")
+            .attr("stroke", "currentColor"));
+    
+        tick = tick.merge(tickEnter);
+    
+        line = line.merge(tickEnter.append("line")
+            .attr("stroke", "currentColor")
+            .attr(x + "2", k * tickSizeInner));
+    
+        text = text.merge(tickEnter.append("text")
+            .attr("fill", "currentColor")
+            .attr(x, k * spacing)
+            .attr("dy", orient === top ? "0em" : orient === bottom ? "0.71em" : "0.32em"));
+    
+        if (context !== selection) {
+          path = path.transition(context);
+          tick = tick.transition(context);
+          line = line.transition(context);
+          text = text.transition(context);
+    
+          tickExit = tickExit.transition(context)
+              .attr("opacity", epsilon$5)
+              .attr("transform", function(d) { return isFinite(d = position(d)) ? transform(d + offset) : this.getAttribute("transform"); });
+    
+          tickEnter
+              .attr("opacity", epsilon$5)
+              .attr("transform", function(d) { var p = this.parentNode.__axis; return transform((p && isFinite(p = p(d)) ? p : position(d)) + offset); });
+        }
+    
+        tickExit.remove();
+    
+        path
+            .attr("d", orient === left || orient === right
+                ? (tickSizeOuter ? "M" + k * tickSizeOuter + "," + range0 + "H" + offset + "V" + range1 + "H" + k * tickSizeOuter : "M" + offset + "," + range0 + "V" + range1)
+                : (tickSizeOuter ? "M" + range0 + "," + k * tickSizeOuter + "V" + offset + "H" + range1 + "V" + k * tickSizeOuter : "M" + range0 + "," + offset + "H" + range1));
+    
+        tick
+            .attr("opacity", 1)
+            .attr("transform", function(d) { return transform(position(d) + offset); });
+    
+        line
+            .attr(x + "2", k * tickSizeInner);
+    
+        text
+            .attr(x, k * spacing)
+            .text(format);
+    
+        selection.filter(entering)
+            .attr("fill", "none")
+            .attr("font-size", 10)
+            .attr("font-family", "sans-serif")
+            .attr("text-anchor", orient === right ? "start" : orient === left ? "end" : "middle");
+    
+        selection
+            .each(function() { this.__axis = position; });
+      }
+    
+      axis.scale = function(_) {
+        return arguments.length ? (scale = _, axis) : scale;
+      };
+    
+      axis.ticks = function() {
+        return tickArguments = slice$3.call(arguments), axis;
+      };
+    
+      axis.tickArguments = function(_) {
+        return arguments.length ? (tickArguments = _ == null ? [] : slice$3.call(_), axis) : tickArguments.slice();
+      };
+    
+      axis.tickValues = function(_) {
+        return arguments.length ? (tickValues = _ == null ? null : slice$3.call(_), axis) : tickValues && tickValues.slice();
+      };
+    
+      axis.tickFormat = function(_) {
+        return arguments.length ? (tickFormat = _, axis) : tickFormat;
+      };
+    
+      axis.tickSize = function(_) {
+        return arguments.length ? (tickSizeInner = tickSizeOuter = +_, axis) : tickSizeInner;
+      };
+    
+      axis.tickSizeInner = function(_) {
+        return arguments.length ? (tickSizeInner = +_, axis) : tickSizeInner;
+      };
+    
+      axis.tickSizeOuter = function(_) {
+        return arguments.length ? (tickSizeOuter = +_, axis) : tickSizeOuter;
+      };
+    
+      axis.tickPadding = function(_) {
+        return arguments.length ? (tickPadding = +_, axis) : tickPadding;
+      };
+    
+      axis.offset = function(_) {
+        return arguments.length ? (offset = +_, axis) : offset;
+      };
+    
+      return axis;
+    }
+    
+    function axisTop(scale) {
+      return axis(top, scale);
+    }
+    
+    function axisRight(scale) {
+      return axis(right, scale);
+    }
+    
+    function axisBottom(scale) {
+      return axis(bottom, scale);
+    }
+    
+    function axisLeft(scale) {
+      return axis(left, scale);
+    }
+    
+    var noop$3 = {value: () => {}};
+    
+    function dispatch() {
+      for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
+        if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
+        _[t] = [];
+      }
+      return new Dispatch(_);
+    }
+    
+    function Dispatch(_) {
+      this._ = _;
+    }
+    
+    function parseTypenames$1(typenames, types) {
+      return typenames.trim().split(/^|\s+/).map(function(t) {
+        var name = "", i = t.indexOf(".");
+        if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
+        if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
+        return {type: t, name: name};
+      });
+    }
+    
+    Dispatch.prototype = dispatch.prototype = {
+      constructor: Dispatch,
+      on: function(typename, callback) {
+        var _ = this._,
+            T = parseTypenames$1(typename + "", _),
+            t,
+            i = -1,
+            n = T.length;
+    
+        // If no callback was specified, return the callback of the given type and name.
+        if (arguments.length < 2) {
+          while (++i < n) if ((t = (typename = T[i]).type) && (t = get$1(_[t], typename.name))) return t;
+          return;
+        }
+    
+        // If a type was specified, set the callback for the given type and name.
+        // Otherwise, if a null callback was specified, remove callbacks of the given name.
+        if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
+        while (++i < n) {
+          if (t = (typename = T[i]).type) _[t] = set$1(_[t], typename.name, callback);
+          else if (callback == null) for (t in _) _[t] = set$1(_[t], typename.name, null);
+        }
+    
+        return this;
+      },
+      copy: function() {
+        var copy = {}, _ = this._;
+        for (var t in _) copy[t] = _[t].slice();
+        return new Dispatch(copy);
+      },
+      call: function(type, that) {
+        if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
+        if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
+        for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
+      },
+      apply: function(type, that, args) {
+        if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
+        for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
+      }
+    };
+    
+    function get$1(type, name) {
+      for (var i = 0, n = type.length, c; i < n; ++i) {
+        if ((c = type[i]).name === name) {
+          return c.value;
+        }
+      }
+    }
+    
+    function set$1(type, name, callback) {
+      for (var i = 0, n = type.length; i < n; ++i) {
+        if (type[i].name === name) {
+          type[i] = noop$3, type = type.slice(0, i).concat(type.slice(i + 1));
+          break;
+        }
+      }
+      if (callback != null) type.push({name: name, value: callback});
+      return type;
+    }
+    
+    var xhtml = "http://www.w3.org/1999/xhtml";
+    
+    var namespaces = {
+      svg: "http://www.w3.org/2000/svg",
+      xhtml: xhtml,
+      xlink: "http://www.w3.org/1999/xlink",
+      xml: "http://www.w3.org/XML/1998/namespace",
+      xmlns: "http://www.w3.org/2000/xmlns/"
+    };
+    
+    function namespace(name) {
+      var prefix = name += "", i = prefix.indexOf(":");
+      if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1);
+      return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins
+    }
+    
+    function creatorInherit(name) {
+      return function() {
+        var document = this.ownerDocument,
+            uri = this.namespaceURI;
+        return uri === xhtml && document.documentElement.namespaceURI === xhtml
+            ? document.createElement(name)
+            : document.createElementNS(uri, name);
+      };
+    }
+    
+    function creatorFixed(fullname) {
+      return function() {
+        return this.ownerDocument.createElementNS(fullname.space, fullname.local);
+      };
+    }
+    
+    function creator(name) {
+      var fullname = namespace(name);
+      return (fullname.local
+          ? creatorFixed
+          : creatorInherit)(fullname);
+    }
+    
+    function none$2() {}
+    
+    function selector(selector) {
+      return selector == null ? none$2 : function() {
+        return this.querySelector(selector);
+      };
+    }
+    
+    function selection_select(select) {
+      if (typeof select !== "function") select = selector(select);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
+          if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
+            if ("__data__" in node) subnode.__data__ = node.__data__;
+            subgroup[i] = subnode;
+          }
+        }
+      }
+    
+      return new Selection$1(subgroups, this._parents);
+    }
+    
+    function array$4(x) {
+      return typeof x === "object" && "length" in x
+        ? x // Array, TypedArray, NodeList, array-like
+        : Array.from(x); // Map, Set, iterable, string, or anything else
+    }
+    
+    function empty$1() {
+      return [];
+    }
+    
+    function selectorAll(selector) {
+      return selector == null ? empty$1 : function() {
+        return this.querySelectorAll(selector);
+      };
+    }
+    
+    function arrayAll(select) {
+      return function() {
+        var group = select.apply(this, arguments);
+        return group == null ? [] : array$4(group);
+      };
+    }
+    
+    function selection_selectAll(select) {
+      if (typeof select === "function") select = arrayAll(select);
+      else select = selectorAll(select);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+          if (node = group[i]) {
+            subgroups.push(select.call(node, node.__data__, i, group));
+            parents.push(node);
+          }
+        }
+      }
+    
+      return new Selection$1(subgroups, parents);
+    }
+    
+    function matcher(selector) {
+      return function() {
+        return this.matches(selector);
+      };
+    }
+    
+    function childMatcher(selector) {
+      return function(node) {
+        return node.matches(selector);
+      };
+    }
+    
+    var find$1 = Array.prototype.find;
+    
+    function childFind(match) {
+      return function() {
+        return find$1.call(this.children, match);
+      };
+    }
+    
+    function childFirst() {
+      return this.firstElementChild;
+    }
+    
+    function selection_selectChild(match) {
+      return this.select(match == null ? childFirst
+          : childFind(typeof match === "function" ? match : childMatcher(match)));
+    }
+    
+    var filter = Array.prototype.filter;
+    
+    function children() {
+      return this.children;
+    }
+    
+    function childrenFilter(match) {
+      return function() {
+        return filter.call(this.children, match);
+      };
+    }
+    
+    function selection_selectChildren(match) {
+      return this.selectAll(match == null ? children
+          : childrenFilter(typeof match === "function" ? match : childMatcher(match)));
+    }
+    
+    function selection_filter(match) {
+      if (typeof match !== "function") match = matcher(match);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
+          if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
+            subgroup.push(node);
+          }
+        }
+      }
+    
+      return new Selection$1(subgroups, this._parents);
+    }
+    
+    function sparse(update) {
+      return new Array(update.length);
+    }
+    
+    function selection_enter() {
+      return new Selection$1(this._enter || this._groups.map(sparse), this._parents);
+    }
+    
+    function EnterNode(parent, datum) {
+      this.ownerDocument = parent.ownerDocument;
+      this.namespaceURI = parent.namespaceURI;
+      this._next = null;
+      this._parent = parent;
+      this.__data__ = datum;
+    }
+    
+    EnterNode.prototype = {
+      constructor: EnterNode,
+      appendChild: function(child) { return this._parent.insertBefore(child, this._next); },
+      insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },
+      querySelector: function(selector) { return this._parent.querySelector(selector); },
+      querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }
+    };
+    
+    function constant$a(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function bindIndex(parent, group, enter, update, exit, data) {
+      var i = 0,
+          node,
+          groupLength = group.length,
+          dataLength = data.length;
+    
+      // Put any non-null nodes that fit into update.
+      // Put any null nodes into enter.
+      // Put any remaining data into enter.
+      for (; i < dataLength; ++i) {
+        if (node = group[i]) {
+          node.__data__ = data[i];
+          update[i] = node;
+        } else {
+          enter[i] = new EnterNode(parent, data[i]);
+        }
+      }
+    
+      // Put any non-null nodes that don’t fit into exit.
+      for (; i < groupLength; ++i) {
+        if (node = group[i]) {
+          exit[i] = node;
+        }
+      }
+    }
+    
+    function bindKey(parent, group, enter, update, exit, data, key) {
+      var i,
+          node,
+          nodeByKeyValue = new Map,
+          groupLength = group.length,
+          dataLength = data.length,
+          keyValues = new Array(groupLength),
+          keyValue;
+    
+      // Compute the key for each node.
+      // If multiple nodes have the same key, the duplicates are added to exit.
+      for (i = 0; i < groupLength; ++i) {
+        if (node = group[i]) {
+          keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + "";
+          if (nodeByKeyValue.has(keyValue)) {
+            exit[i] = node;
+          } else {
+            nodeByKeyValue.set(keyValue, node);
+          }
+        }
+      }
+    
+      // Compute the key for each datum.
+      // If there a node associated with this key, join and add it to update.
+      // If there is not (or the key is a duplicate), add it to enter.
+      for (i = 0; i < dataLength; ++i) {
+        keyValue = key.call(parent, data[i], i, data) + "";
+        if (node = nodeByKeyValue.get(keyValue)) {
+          update[i] = node;
+          node.__data__ = data[i];
+          nodeByKeyValue.delete(keyValue);
+        } else {
+          enter[i] = new EnterNode(parent, data[i]);
+        }
+      }
+    
+      // Add any remaining nodes that were not bound to data to exit.
+      for (i = 0; i < groupLength; ++i) {
+        if ((node = group[i]) && (nodeByKeyValue.get(keyValues[i]) === node)) {
+          exit[i] = node;
+        }
+      }
+    }
+    
+    function datum(node) {
+      return node.__data__;
+    }
+    
+    function selection_data(value, key) {
+      if (!arguments.length) return Array.from(this, datum);
+    
+      var bind = key ? bindKey : bindIndex,
+          parents = this._parents,
+          groups = this._groups;
+    
+      if (typeof value !== "function") value = constant$a(value);
+    
+      for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
+        var parent = parents[j],
+            group = groups[j],
+            groupLength = group.length,
+            data = array$4(value.call(parent, parent && parent.__data__, j, parents)),
+            dataLength = data.length,
+            enterGroup = enter[j] = new Array(dataLength),
+            updateGroup = update[j] = new Array(dataLength),
+            exitGroup = exit[j] = new Array(groupLength);
+    
+        bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
+    
+        // Now connect the enter nodes to their following update node, such that
+        // appendChild can insert the materialized enter node before this node,
+        // rather than at the end of the parent node.
+        for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
+          if (previous = enterGroup[i0]) {
+            if (i0 >= i1) i1 = i0 + 1;
+            while (!(next = updateGroup[i1]) && ++i1 < dataLength);
+            previous._next = next || null;
+          }
+        }
+      }
+    
+      update = new Selection$1(update, parents);
+      update._enter = enter;
+      update._exit = exit;
+      return update;
+    }
+    
+    function selection_exit() {
+      return new Selection$1(this._exit || this._groups.map(sparse), this._parents);
+    }
+    
+    function selection_join(onenter, onupdate, onexit) {
+      var enter = this.enter(), update = this, exit = this.exit();
+      enter = typeof onenter === "function" ? onenter(enter) : enter.append(onenter + "");
+      if (onupdate != null) update = onupdate(update);
+      if (onexit == null) exit.remove(); else onexit(exit);
+      return enter && update ? enter.merge(update).order() : update;
+    }
+    
+    function selection_merge(selection) {
+      if (!(selection instanceof Selection$1)) throw new Error("invalid merge");
+    
+      for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
+        for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
+          if (node = group0[i] || group1[i]) {
+            merge[i] = node;
+          }
+        }
+      }
+    
+      for (; j < m0; ++j) {
+        merges[j] = groups0[j];
+      }
+    
+      return new Selection$1(merges, this._parents);
+    }
+    
+    function selection_order() {
+    
+      for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
+        for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
+          if (node = group[i]) {
+            if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next);
+            next = node;
+          }
+        }
+      }
+    
+      return this;
+    }
+    
+    function selection_sort(compare) {
+      if (!compare) compare = ascending$2;
+    
+      function compareNode(a, b) {
+        return a && b ? compare(a.__data__, b.__data__) : !a - !b;
+      }
+    
+      for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
+          if (node = group[i]) {
+            sortgroup[i] = node;
+          }
+        }
+        sortgroup.sort(compareNode);
+      }
+    
+      return new Selection$1(sortgroups, this._parents).order();
+    }
+    
+    function ascending$2(a, b) {
+      return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+    }
+    
+    function selection_call() {
+      var callback = arguments[0];
+      arguments[0] = this;
+      callback.apply(null, arguments);
+      return this;
+    }
+    
+    function selection_nodes() {
+      return Array.from(this);
+    }
+    
+    function selection_node() {
+    
+      for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
+        for (var group = groups[j], i = 0, n = group.length; i < n; ++i) {
+          var node = group[i];
+          if (node) return node;
+        }
+      }
+    
+      return null;
+    }
+    
+    function selection_size() {
+      let size = 0;
+      for (const node of this) ++size; // eslint-disable-line no-unused-vars
+      return size;
+    }
+    
+    function selection_empty() {
+      return !this.node();
+    }
+    
+    function selection_each(callback) {
+    
+      for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
+        for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
+          if (node = group[i]) callback.call(node, node.__data__, i, group);
+        }
+      }
+    
+      return this;
+    }
+    
+    function attrRemove$1(name) {
+      return function() {
+        this.removeAttribute(name);
+      };
+    }
+    
+    function attrRemoveNS$1(fullname) {
+      return function() {
+        this.removeAttributeNS(fullname.space, fullname.local);
+      };
+    }
+    
+    function attrConstant$1(name, value) {
+      return function() {
+        this.setAttribute(name, value);
+      };
+    }
+    
+    function attrConstantNS$1(fullname, value) {
+      return function() {
+        this.setAttributeNS(fullname.space, fullname.local, value);
+      };
+    }
+    
+    function attrFunction$1(name, value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        if (v == null) this.removeAttribute(name);
+        else this.setAttribute(name, v);
+      };
+    }
+    
+    function attrFunctionNS$1(fullname, value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        if (v == null) this.removeAttributeNS(fullname.space, fullname.local);
+        else this.setAttributeNS(fullname.space, fullname.local, v);
+      };
+    }
+    
+    function selection_attr(name, value) {
+      var fullname = namespace(name);
+    
+      if (arguments.length < 2) {
+        var node = this.node();
+        return fullname.local
+            ? node.getAttributeNS(fullname.space, fullname.local)
+            : node.getAttribute(fullname);
+      }
+    
+      return this.each((value == null
+          ? (fullname.local ? attrRemoveNS$1 : attrRemove$1) : (typeof value === "function"
+          ? (fullname.local ? attrFunctionNS$1 : attrFunction$1)
+          : (fullname.local ? attrConstantNS$1 : attrConstant$1)))(fullname, value));
+    }
+    
+    function defaultView(node) {
+      return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node
+          || (node.document && node) // node is a Window
+          || node.defaultView; // node is a Document
+    }
+    
+    function styleRemove$1(name) {
+      return function() {
+        this.style.removeProperty(name);
+      };
+    }
+    
+    function styleConstant$1(name, value, priority) {
+      return function() {
+        this.style.setProperty(name, value, priority);
+      };
+    }
+    
+    function styleFunction$1(name, value, priority) {
+      return function() {
+        var v = value.apply(this, arguments);
+        if (v == null) this.style.removeProperty(name);
+        else this.style.setProperty(name, v, priority);
+      };
+    }
+    
+    function selection_style(name, value, priority) {
+      return arguments.length > 1
+          ? this.each((value == null
+                ? styleRemove$1 : typeof value === "function"
+                ? styleFunction$1
+                : styleConstant$1)(name, value, priority == null ? "" : priority))
+          : styleValue(this.node(), name);
+    }
+    
+    function styleValue(node, name) {
+      return node.style.getPropertyValue(name)
+          || defaultView(node).getComputedStyle(node, null).getPropertyValue(name);
+    }
+    
+    function propertyRemove(name) {
+      return function() {
+        delete this[name];
+      };
+    }
+    
+    function propertyConstant(name, value) {
+      return function() {
+        this[name] = value;
+      };
+    }
+    
+    function propertyFunction(name, value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        if (v == null) delete this[name];
+        else this[name] = v;
+      };
+    }
+    
+    function selection_property(name, value) {
+      return arguments.length > 1
+          ? this.each((value == null
+              ? propertyRemove : typeof value === "function"
+              ? propertyFunction
+              : propertyConstant)(name, value))
+          : this.node()[name];
+    }
+    
+    function classArray(string) {
+      return string.trim().split(/^|\s+/);
+    }
+    
+    function classList(node) {
+      return node.classList || new ClassList(node);
+    }
+    
+    function ClassList(node) {
+      this._node = node;
+      this._names = classArray(node.getAttribute("class") || "");
+    }
+    
+    ClassList.prototype = {
+      add: function(name) {
+        var i = this._names.indexOf(name);
+        if (i < 0) {
+          this._names.push(name);
+          this._node.setAttribute("class", this._names.join(" "));
+        }
+      },
+      remove: function(name) {
+        var i = this._names.indexOf(name);
+        if (i >= 0) {
+          this._names.splice(i, 1);
+          this._node.setAttribute("class", this._names.join(" "));
+        }
+      },
+      contains: function(name) {
+        return this._names.indexOf(name) >= 0;
+      }
+    };
+    
+    function classedAdd(node, names) {
+      var list = classList(node), i = -1, n = names.length;
+      while (++i < n) list.add(names[i]);
+    }
+    
+    function classedRemove(node, names) {
+      var list = classList(node), i = -1, n = names.length;
+      while (++i < n) list.remove(names[i]);
+    }
+    
+    function classedTrue(names) {
+      return function() {
+        classedAdd(this, names);
+      };
+    }
+    
+    function classedFalse(names) {
+      return function() {
+        classedRemove(this, names);
+      };
+    }
+    
+    function classedFunction(names, value) {
+      return function() {
+        (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names);
+      };
+    }
+    
+    function selection_classed(name, value) {
+      var names = classArray(name + "");
+    
+      if (arguments.length < 2) {
+        var list = classList(this.node()), i = -1, n = names.length;
+        while (++i < n) if (!list.contains(names[i])) return false;
+        return true;
+      }
+    
+      return this.each((typeof value === "function"
+          ? classedFunction : value
+          ? classedTrue
+          : classedFalse)(names, value));
+    }
+    
+    function textRemove() {
+      this.textContent = "";
+    }
+    
+    function textConstant$1(value) {
+      return function() {
+        this.textContent = value;
+      };
+    }
+    
+    function textFunction$1(value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        this.textContent = v == null ? "" : v;
+      };
+    }
+    
+    function selection_text(value) {
+      return arguments.length
+          ? this.each(value == null
+              ? textRemove : (typeof value === "function"
+              ? textFunction$1
+              : textConstant$1)(value))
+          : this.node().textContent;
+    }
+    
+    function htmlRemove() {
+      this.innerHTML = "";
+    }
+    
+    function htmlConstant(value) {
+      return function() {
+        this.innerHTML = value;
+      };
+    }
+    
+    function htmlFunction(value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        this.innerHTML = v == null ? "" : v;
+      };
+    }
+    
+    function selection_html(value) {
+      return arguments.length
+          ? this.each(value == null
+              ? htmlRemove : (typeof value === "function"
+              ? htmlFunction
+              : htmlConstant)(value))
+          : this.node().innerHTML;
+    }
+    
+    function raise() {
+      if (this.nextSibling) this.parentNode.appendChild(this);
+    }
+    
+    function selection_raise() {
+      return this.each(raise);
+    }
+    
+    function lower() {
+      if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild);
+    }
+    
+    function selection_lower() {
+      return this.each(lower);
+    }
+    
+    function selection_append(name) {
+      var create = typeof name === "function" ? name : creator(name);
+      return this.select(function() {
+        return this.appendChild(create.apply(this, arguments));
+      });
+    }
+    
+    function constantNull() {
+      return null;
+    }
+    
+    function selection_insert(name, before) {
+      var create = typeof name === "function" ? name : creator(name),
+          select = before == null ? constantNull : typeof before === "function" ? before : selector(before);
+      return this.select(function() {
+        return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);
+      });
+    }
+    
+    function remove() {
+      var parent = this.parentNode;
+      if (parent) parent.removeChild(this);
+    }
+    
+    function selection_remove() {
+      return this.each(remove);
+    }
+    
+    function selection_cloneShallow() {
+      var clone = this.cloneNode(false), parent = this.parentNode;
+      return parent ? parent.insertBefore(clone, this.nextSibling) : clone;
+    }
+    
+    function selection_cloneDeep() {
+      var clone = this.cloneNode(true), parent = this.parentNode;
+      return parent ? parent.insertBefore(clone, this.nextSibling) : clone;
+    }
+    
+    function selection_clone(deep) {
+      return this.select(deep ? selection_cloneDeep : selection_cloneShallow);
+    }
+    
+    function selection_datum(value) {
+      return arguments.length
+          ? this.property("__data__", value)
+          : this.node().__data__;
+    }
+    
+    function contextListener(listener) {
+      return function(event) {
+        listener.call(this, event, this.__data__);
+      };
+    }
+    
+    function parseTypenames(typenames) {
+      return typenames.trim().split(/^|\s+/).map(function(t) {
+        var name = "", i = t.indexOf(".");
+        if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
+        return {type: t, name: name};
+      });
+    }
+    
+    function onRemove(typename) {
+      return function() {
+        var on = this.__on;
+        if (!on) return;
+        for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
+          if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
+            this.removeEventListener(o.type, o.listener, o.options);
+          } else {
+            on[++i] = o;
+          }
+        }
+        if (++i) on.length = i;
+        else delete this.__on;
+      };
+    }
+    
+    function onAdd(typename, value, options) {
+      return function() {
+        var on = this.__on, o, listener = contextListener(value);
+        if (on) for (var j = 0, m = on.length; j < m; ++j) {
+          if ((o = on[j]).type === typename.type && o.name === typename.name) {
+            this.removeEventListener(o.type, o.listener, o.options);
+            this.addEventListener(o.type, o.listener = listener, o.options = options);
+            o.value = value;
+            return;
+          }
+        }
+        this.addEventListener(typename.type, listener, options);
+        o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options};
+        if (!on) this.__on = [o];
+        else on.push(o);
+      };
+    }
+    
+    function selection_on(typename, value, options) {
+      var typenames = parseTypenames(typename + ""), i, n = typenames.length, t;
+    
+      if (arguments.length < 2) {
+        var on = this.node().__on;
+        if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
+          for (i = 0, o = on[j]; i < n; ++i) {
+            if ((t = typenames[i]).type === o.type && t.name === o.name) {
+              return o.value;
+            }
+          }
+        }
+        return;
+      }
+    
+      on = value ? onAdd : onRemove;
+      for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));
+      return this;
+    }
+    
+    function dispatchEvent(node, type, params) {
+      var window = defaultView(node),
+          event = window.CustomEvent;
+    
+      if (typeof event === "function") {
+        event = new event(type, params);
+      } else {
+        event = window.document.createEvent("Event");
+        if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
+        else event.initEvent(type, false, false);
+      }
+    
+      node.dispatchEvent(event);
+    }
+    
+    function dispatchConstant(type, params) {
+      return function() {
+        return dispatchEvent(this, type, params);
+      };
+    }
+    
+    function dispatchFunction(type, params) {
+      return function() {
+        return dispatchEvent(this, type, params.apply(this, arguments));
+      };
+    }
+    
+    function selection_dispatch(type, params) {
+      return this.each((typeof params === "function"
+          ? dispatchFunction
+          : dispatchConstant)(type, params));
+    }
+    
+    function* selection_iterator() {
+      for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
+        for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
+          if (node = group[i]) yield node;
+        }
+      }
+    }
+    
+    var root$1 = [null];
+    
+    function Selection$1(groups, parents) {
+      this._groups = groups;
+      this._parents = parents;
+    }
+    
+    function selection() {
+      return new Selection$1([[document.documentElement]], root$1);
+    }
+    
+    function selection_selection() {
+      return this;
+    }
+    
+    Selection$1.prototype = selection.prototype = {
+      constructor: Selection$1,
+      select: selection_select,
+      selectAll: selection_selectAll,
+      selectChild: selection_selectChild,
+      selectChildren: selection_selectChildren,
+      filter: selection_filter,
+      data: selection_data,
+      enter: selection_enter,
+      exit: selection_exit,
+      join: selection_join,
+      merge: selection_merge,
+      selection: selection_selection,
+      order: selection_order,
+      sort: selection_sort,
+      call: selection_call,
+      nodes: selection_nodes,
+      node: selection_node,
+      size: selection_size,
+      empty: selection_empty,
+      each: selection_each,
+      attr: selection_attr,
+      style: selection_style,
+      property: selection_property,
+      classed: selection_classed,
+      text: selection_text,
+      html: selection_html,
+      raise: selection_raise,
+      lower: selection_lower,
+      append: selection_append,
+      insert: selection_insert,
+      remove: selection_remove,
+      clone: selection_clone,
+      datum: selection_datum,
+      on: selection_on,
+      dispatch: selection_dispatch,
+      [Symbol.iterator]: selection_iterator
+    };
+    
+    function select(selector) {
+      return typeof selector === "string"
+          ? new Selection$1([[document.querySelector(selector)]], [document.documentElement])
+          : new Selection$1([[selector]], root$1);
+    }
+    
+    function create$1(name) {
+      return select(creator(name).call(document.documentElement));
+    }
+    
+    var nextId = 0;
+    
+    function local$1() {
+      return new Local;
+    }
+    
+    function Local() {
+      this._ = "@" + (++nextId).toString(36);
+    }
+    
+    Local.prototype = local$1.prototype = {
+      constructor: Local,
+      get: function(node) {
+        var id = this._;
+        while (!(id in node)) if (!(node = node.parentNode)) return;
+        return node[id];
+      },
+      set: function(node, value) {
+        return node[this._] = value;
+      },
+      remove: function(node) {
+        return this._ in node && delete node[this._];
+      },
+      toString: function() {
+        return this._;
+      }
+    };
+    
+    function sourceEvent(event) {
+      let sourceEvent;
+      while (sourceEvent = event.sourceEvent) event = sourceEvent;
+      return event;
+    }
+    
+    function pointer(event, node) {
+      event = sourceEvent(event);
+      if (node === undefined) node = event.currentTarget;
+      if (node) {
+        var svg = node.ownerSVGElement || node;
+        if (svg.createSVGPoint) {
+          var point = svg.createSVGPoint();
+          point.x = event.clientX, point.y = event.clientY;
+          point = point.matrixTransform(node.getScreenCTM().inverse());
+          return [point.x, point.y];
+        }
+        if (node.getBoundingClientRect) {
+          var rect = node.getBoundingClientRect();
+          return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
+        }
+      }
+      return [event.pageX, event.pageY];
+    }
+    
+    function pointers(events, node) {
+      if (events.target) { // i.e., instanceof Event, not TouchList or iterable
+        events = sourceEvent(events);
+        if (node === undefined) node = events.currentTarget;
+        events = events.touches || [events];
+      }
+      return Array.from(events, event => pointer(event, node));
+    }
+    
+    function selectAll(selector) {
+      return typeof selector === "string"
+          ? new Selection$1([document.querySelectorAll(selector)], [document.documentElement])
+          : new Selection$1([selector == null ? [] : array$4(selector)], root$1);
+    }
+    
+    function nopropagation$2(event) {
+      event.stopImmediatePropagation();
+    }
+    
+    function noevent$2(event) {
+      event.preventDefault();
+      event.stopImmediatePropagation();
+    }
+    
+    function dragDisable(view) {
+      var root = view.document.documentElement,
+          selection = select(view).on("dragstart.drag", noevent$2, true);
+      if ("onselectstart" in root) {
+        selection.on("selectstart.drag", noevent$2, true);
+      } else {
+        root.__noselect = root.style.MozUserSelect;
+        root.style.MozUserSelect = "none";
+      }
+    }
+    
+    function yesdrag(view, noclick) {
+      var root = view.document.documentElement,
+          selection = select(view).on("dragstart.drag", null);
+      if (noclick) {
+        selection.on("click.drag", noevent$2, true);
+        setTimeout(function() { selection.on("click.drag", null); }, 0);
+      }
+      if ("onselectstart" in root) {
+        selection.on("selectstart.drag", null);
+      } else {
+        root.style.MozUserSelect = root.__noselect;
+        delete root.__noselect;
+      }
+    }
+    
+    var constant$9 = x => () => x;
+    
+    function DragEvent(type, {
+      sourceEvent,
+      subject,
+      target,
+      identifier,
+      active,
+      x, y, dx, dy,
+      dispatch
+    }) {
+      Object.defineProperties(this, {
+        type: {value: type, enumerable: true, configurable: true},
+        sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},
+        subject: {value: subject, enumerable: true, configurable: true},
+        target: {value: target, enumerable: true, configurable: true},
+        identifier: {value: identifier, enumerable: true, configurable: true},
+        active: {value: active, enumerable: true, configurable: true},
+        x: {value: x, enumerable: true, configurable: true},
+        y: {value: y, enumerable: true, configurable: true},
+        dx: {value: dx, enumerable: true, configurable: true},
+        dy: {value: dy, enumerable: true, configurable: true},
+        _: {value: dispatch}
+      });
+    }
+    
+    DragEvent.prototype.on = function() {
+      var value = this._.on.apply(this._, arguments);
+      return value === this._ ? this : value;
+    };
+    
+    // Ignore right-click, since that should open the context menu.
+    function defaultFilter$2(event) {
+      return !event.ctrlKey && !event.button;
+    }
+    
+    function defaultContainer() {
+      return this.parentNode;
+    }
+    
+    function defaultSubject(event, d) {
+      return d == null ? {x: event.x, y: event.y} : d;
+    }
+    
+    function defaultTouchable$2() {
+      return navigator.maxTouchPoints || ("ontouchstart" in this);
+    }
+    
+    function drag() {
+      var filter = defaultFilter$2,
+          container = defaultContainer,
+          subject = defaultSubject,
+          touchable = defaultTouchable$2,
+          gestures = {},
+          listeners = dispatch("start", "drag", "end"),
+          active = 0,
+          mousedownx,
+          mousedowny,
+          mousemoving,
+          touchending,
+          clickDistance2 = 0;
+    
+      function drag(selection) {
+        selection
+            .on("mousedown.drag", mousedowned)
+          .filter(touchable)
+            .on("touchstart.drag", touchstarted)
+            .on("touchmove.drag", touchmoved)
+            .on("touchend.drag touchcancel.drag", touchended)
+            .style("touch-action", "none")
+            .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+      }
+    
+      function mousedowned(event, d) {
+        if (touchending || !filter.call(this, event, d)) return;
+        var gesture = beforestart(this, container.call(this, event, d), event, d, "mouse");
+        if (!gesture) return;
+        select(event.view).on("mousemove.drag", mousemoved, true).on("mouseup.drag", mouseupped, true);
+        dragDisable(event.view);
+        nopropagation$2(event);
+        mousemoving = false;
+        mousedownx = event.clientX;
+        mousedowny = event.clientY;
+        gesture("start", event);
+      }
+    
+      function mousemoved(event) {
+        noevent$2(event);
+        if (!mousemoving) {
+          var dx = event.clientX - mousedownx, dy = event.clientY - mousedowny;
+          mousemoving = dx * dx + dy * dy > clickDistance2;
+        }
+        gestures.mouse("drag", event);
+      }
+    
+      function mouseupped(event) {
+        select(event.view).on("mousemove.drag mouseup.drag", null);
+        yesdrag(event.view, mousemoving);
+        noevent$2(event);
+        gestures.mouse("end", event);
+      }
+    
+      function touchstarted(event, d) {
+        if (!filter.call(this, event, d)) return;
+        var touches = event.changedTouches,
+            c = container.call(this, event, d),
+            n = touches.length, i, gesture;
+    
+        for (i = 0; i < n; ++i) {
+          if (gesture = beforestart(this, c, event, d, touches[i].identifier, touches[i])) {
+            nopropagation$2(event);
+            gesture("start", event, touches[i]);
+          }
+        }
+      }
+    
+      function touchmoved(event) {
+        var touches = event.changedTouches,
+            n = touches.length, i, gesture;
+    
+        for (i = 0; i < n; ++i) {
+          if (gesture = gestures[touches[i].identifier]) {
+            noevent$2(event);
+            gesture("drag", event, touches[i]);
+          }
+        }
+      }
+    
+      function touchended(event) {
+        var touches = event.changedTouches,
+            n = touches.length, i, gesture;
+    
+        if (touchending) clearTimeout(touchending);
+        touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
+        for (i = 0; i < n; ++i) {
+          if (gesture = gestures[touches[i].identifier]) {
+            nopropagation$2(event);
+            gesture("end", event, touches[i]);
+          }
+        }
+      }
+    
+      function beforestart(that, container, event, d, identifier, touch) {
+        var dispatch = listeners.copy(),
+            p = pointer(touch || event, container), dx, dy,
+            s;
+    
+        if ((s = subject.call(that, new DragEvent("beforestart", {
+            sourceEvent: event,
+            target: drag,
+            identifier,
+            active,
+            x: p[0],
+            y: p[1],
+            dx: 0,
+            dy: 0,
+            dispatch
+          }), d)) == null) return;
+    
+        dx = s.x - p[0] || 0;
+        dy = s.y - p[1] || 0;
+    
+        return function gesture(type, event, touch) {
+          var p0 = p, n;
+          switch (type) {
+            case "start": gestures[identifier] = gesture, n = active++; break;
+            case "end": delete gestures[identifier], --active; // nobreak
+            case "drag": p = pointer(touch || event, container), n = active; break;
+          }
+          dispatch.call(
+            type,
+            that,
+            new DragEvent(type, {
+              sourceEvent: event,
+              subject: s,
+              target: drag,
+              identifier,
+              active: n,
+              x: p[0] + dx,
+              y: p[1] + dy,
+              dx: p[0] - p0[0],
+              dy: p[1] - p0[1],
+              dispatch
+            }),
+            d
+          );
+        };
+      }
+    
+      drag.filter = function(_) {
+        return arguments.length ? (filter = typeof _ === "function" ? _ : constant$9(!!_), drag) : filter;
+      };
+    
+      drag.container = function(_) {
+        return arguments.length ? (container = typeof _ === "function" ? _ : constant$9(_), drag) : container;
+      };
+    
+      drag.subject = function(_) {
+        return arguments.length ? (subject = typeof _ === "function" ? _ : constant$9(_), drag) : subject;
+      };
+    
+      drag.touchable = function(_) {
+        return arguments.length ? (touchable = typeof _ === "function" ? _ : constant$9(!!_), drag) : touchable;
+      };
+    
+      drag.on = function() {
+        var value = listeners.on.apply(listeners, arguments);
+        return value === listeners ? drag : value;
+      };
+    
+      drag.clickDistance = function(_) {
+        return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
+      };
+    
+      return drag;
+    }
+    
+    function define(constructor, factory, prototype) {
+      constructor.prototype = factory.prototype = prototype;
+      prototype.constructor = constructor;
+    }
+    
+    function extend(parent, definition) {
+      var prototype = Object.create(parent.prototype);
+      for (var key in definition) prototype[key] = definition[key];
+      return prototype;
+    }
+    
+    function Color() {}
+    
+    var darker = 0.7;
+    var brighter = 1 / darker;
+    
+    var reI = "\\s*([+-]?\\d+)\\s*",
+        reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",
+        reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",
+        reHex = /^#([0-9a-f]{3,8})$/,
+        reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"),
+        reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"),
+        reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"),
+        reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"),
+        reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"),
+        reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$");
+    
+    var named = {
+      aliceblue: 0xf0f8ff,
+      antiquewhite: 0xfaebd7,
+      aqua: 0x00ffff,
+      aquamarine: 0x7fffd4,
+      azure: 0xf0ffff,
+      beige: 0xf5f5dc,
+      bisque: 0xffe4c4,
+      black: 0x000000,
+      blanchedalmond: 0xffebcd,
+      blue: 0x0000ff,
+      blueviolet: 0x8a2be2,
+      brown: 0xa52a2a,
+      burlywood: 0xdeb887,
+      cadetblue: 0x5f9ea0,
+      chartreuse: 0x7fff00,
+      chocolate: 0xd2691e,
+      coral: 0xff7f50,
+      cornflowerblue: 0x6495ed,
+      cornsilk: 0xfff8dc,
+      crimson: 0xdc143c,
+      cyan: 0x00ffff,
+      darkblue: 0x00008b,
+      darkcyan: 0x008b8b,
+      darkgoldenrod: 0xb8860b,
+      darkgray: 0xa9a9a9,
+      darkgreen: 0x006400,
+      darkgrey: 0xa9a9a9,
+      darkkhaki: 0xbdb76b,
+      darkmagenta: 0x8b008b,
+      darkolivegreen: 0x556b2f,
+      darkorange: 0xff8c00,
+      darkorchid: 0x9932cc,
+      darkred: 0x8b0000,
+      darksalmon: 0xe9967a,
+      darkseagreen: 0x8fbc8f,
+      darkslateblue: 0x483d8b,
+      darkslategray: 0x2f4f4f,
+      darkslategrey: 0x2f4f4f,
+      darkturquoise: 0x00ced1,
+      darkviolet: 0x9400d3,
+      deeppink: 0xff1493,
+      deepskyblue: 0x00bfff,
+      dimgray: 0x696969,
+      dimgrey: 0x696969,
+      dodgerblue: 0x1e90ff,
+      firebrick: 0xb22222,
+      floralwhite: 0xfffaf0,
+      forestgreen: 0x228b22,
+      fuchsia: 0xff00ff,
+      gainsboro: 0xdcdcdc,
+      ghostwhite: 0xf8f8ff,
+      gold: 0xffd700,
+      goldenrod: 0xdaa520,
+      gray: 0x808080,
+      green: 0x008000,
+      greenyellow: 0xadff2f,
+      grey: 0x808080,
+      honeydew: 0xf0fff0,
+      hotpink: 0xff69b4,
+      indianred: 0xcd5c5c,
+      indigo: 0x4b0082,
+      ivory: 0xfffff0,
+      khaki: 0xf0e68c,
+      lavender: 0xe6e6fa,
+      lavenderblush: 0xfff0f5,
+      lawngreen: 0x7cfc00,
+      lemonchiffon: 0xfffacd,
+      lightblue: 0xadd8e6,
+      lightcoral: 0xf08080,
+      lightcyan: 0xe0ffff,
+      lightgoldenrodyellow: 0xfafad2,
+      lightgray: 0xd3d3d3,
+      lightgreen: 0x90ee90,
+      lightgrey: 0xd3d3d3,
+      lightpink: 0xffb6c1,
+      lightsalmon: 0xffa07a,
+      lightseagreen: 0x20b2aa,
+      lightskyblue: 0x87cefa,
+      lightslategray: 0x778899,
+      lightslategrey: 0x778899,
+      lightsteelblue: 0xb0c4de,
+      lightyellow: 0xffffe0,
+      lime: 0x00ff00,
+      limegreen: 0x32cd32,
+      linen: 0xfaf0e6,
+      magenta: 0xff00ff,
+      maroon: 0x800000,
+      mediumaquamarine: 0x66cdaa,
+      mediumblue: 0x0000cd,
+      mediumorchid: 0xba55d3,
+      mediumpurple: 0x9370db,
+      mediumseagreen: 0x3cb371,
+      mediumslateblue: 0x7b68ee,
+      mediumspringgreen: 0x00fa9a,
+      mediumturquoise: 0x48d1cc,
+      mediumvioletred: 0xc71585,
+      midnightblue: 0x191970,
+      mintcream: 0xf5fffa,
+      mistyrose: 0xffe4e1,
+      moccasin: 0xffe4b5,
+      navajowhite: 0xffdead,
+      navy: 0x000080,
+      oldlace: 0xfdf5e6,
+      olive: 0x808000,
+      olivedrab: 0x6b8e23,
+      orange: 0xffa500,
+      orangered: 0xff4500,
+      orchid: 0xda70d6,
+      palegoldenrod: 0xeee8aa,
+      palegreen: 0x98fb98,
+      paleturquoise: 0xafeeee,
+      palevioletred: 0xdb7093,
+      papayawhip: 0xffefd5,
+      peachpuff: 0xffdab9,
+      peru: 0xcd853f,
+      pink: 0xffc0cb,
+      plum: 0xdda0dd,
+      powderblue: 0xb0e0e6,
+      purple: 0x800080,
+      rebeccapurple: 0x663399,
+      red: 0xff0000,
+      rosybrown: 0xbc8f8f,
+      royalblue: 0x4169e1,
+      saddlebrown: 0x8b4513,
+      salmon: 0xfa8072,
+      sandybrown: 0xf4a460,
+      seagreen: 0x2e8b57,
+      seashell: 0xfff5ee,
+      sienna: 0xa0522d,
+      silver: 0xc0c0c0,
+      skyblue: 0x87ceeb,
+      slateblue: 0x6a5acd,
+      slategray: 0x708090,
+      slategrey: 0x708090,
+      snow: 0xfffafa,
+      springgreen: 0x00ff7f,
+      steelblue: 0x4682b4,
+      tan: 0xd2b48c,
+      teal: 0x008080,
+      thistle: 0xd8bfd8,
+      tomato: 0xff6347,
+      turquoise: 0x40e0d0,
+      violet: 0xee82ee,
+      wheat: 0xf5deb3,
+      white: 0xffffff,
+      whitesmoke: 0xf5f5f5,
+      yellow: 0xffff00,
+      yellowgreen: 0x9acd32
+    };
+    
+    define(Color, color, {
+      copy: function(channels) {
+        return Object.assign(new this.constructor, this, channels);
+      },
+      displayable: function() {
+        return this.rgb().displayable();
+      },
+      hex: color_formatHex, // Deprecated! Use color.formatHex.
+      formatHex: color_formatHex,
+      formatHsl: color_formatHsl,
+      formatRgb: color_formatRgb,
+      toString: color_formatRgb
+    });
+    
+    function color_formatHex() {
+      return this.rgb().formatHex();
+    }
+    
+    function color_formatHsl() {
+      return hslConvert(this).formatHsl();
+    }
+    
+    function color_formatRgb() {
+      return this.rgb().formatRgb();
+    }
+    
+    function color(format) {
+      var m, l;
+      format = (format + "").trim().toLowerCase();
+      return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000
+          : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00
+          : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000
+          : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000
+          : null) // invalid hex
+          : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)
+          : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)
+          : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)
+          : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)
+          : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)
+          : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)
+          : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins
+          : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0)
+          : null;
+    }
+    
+    function rgbn(n) {
+      return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);
+    }
+    
+    function rgba(r, g, b, a) {
+      if (a <= 0) r = g = b = NaN;
+      return new Rgb(r, g, b, a);
+    }
+    
+    function rgbConvert(o) {
+      if (!(o instanceof Color)) o = color(o);
+      if (!o) return new Rgb;
+      o = o.rgb();
+      return new Rgb(o.r, o.g, o.b, o.opacity);
+    }
+    
+    function rgb(r, g, b, opacity) {
+      return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);
+    }
+    
+    function Rgb(r, g, b, opacity) {
+      this.r = +r;
+      this.g = +g;
+      this.b = +b;
+      this.opacity = +opacity;
+    }
+    
+    define(Rgb, rgb, extend(Color, {
+      brighter: function(k) {
+        k = k == null ? brighter : Math.pow(brighter, k);
+        return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
+      },
+      darker: function(k) {
+        k = k == null ? darker : Math.pow(darker, k);
+        return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
+      },
+      rgb: function() {
+        return this;
+      },
+      displayable: function() {
+        return (-0.5 <= this.r && this.r < 255.5)
+            && (-0.5 <= this.g && this.g < 255.5)
+            && (-0.5 <= this.b && this.b < 255.5)
+            && (0 <= this.opacity && this.opacity <= 1);
+      },
+      hex: rgb_formatHex, // Deprecated! Use color.formatHex.
+      formatHex: rgb_formatHex,
+      formatRgb: rgb_formatRgb,
+      toString: rgb_formatRgb
+    }));
+    
+    function rgb_formatHex() {
+      return "#" + hex(this.r) + hex(this.g) + hex(this.b);
+    }
+    
+    function rgb_formatRgb() {
+      var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
+      return (a === 1 ? "rgb(" : "rgba(")
+          + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", "
+          + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", "
+          + Math.max(0, Math.min(255, Math.round(this.b) || 0))
+          + (a === 1 ? ")" : ", " + a + ")");
+    }
+    
+    function hex(value) {
+      value = Math.max(0, Math.min(255, Math.round(value) || 0));
+      return (value < 16 ? "0" : "") + value.toString(16);
+    }
+    
+    function hsla(h, s, l, a) {
+      if (a <= 0) h = s = l = NaN;
+      else if (l <= 0 || l >= 1) h = s = NaN;
+      else if (s <= 0) h = NaN;
+      return new Hsl(h, s, l, a);
+    }
+    
+    function hslConvert(o) {
+      if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);
+      if (!(o instanceof Color)) o = color(o);
+      if (!o) return new Hsl;
+      if (o instanceof Hsl) return o;
+      o = o.rgb();
+      var r = o.r / 255,
+          g = o.g / 255,
+          b = o.b / 255,
+          min = Math.min(r, g, b),
+          max = Math.max(r, g, b),
+          h = NaN,
+          s = max - min,
+          l = (max + min) / 2;
+      if (s) {
+        if (r === max) h = (g - b) / s + (g < b) * 6;
+        else if (g === max) h = (b - r) / s + 2;
+        else h = (r - g) / s + 4;
+        s /= l < 0.5 ? max + min : 2 - max - min;
+        h *= 60;
+      } else {
+        s = l > 0 && l < 1 ? 0 : h;
+      }
+      return new Hsl(h, s, l, o.opacity);
+    }
+    
+    function hsl$2(h, s, l, opacity) {
+      return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);
+    }
+    
+    function Hsl(h, s, l, opacity) {
+      this.h = +h;
+      this.s = +s;
+      this.l = +l;
+      this.opacity = +opacity;
+    }
+    
+    define(Hsl, hsl$2, extend(Color, {
+      brighter: function(k) {
+        k = k == null ? brighter : Math.pow(brighter, k);
+        return new Hsl(this.h, this.s, this.l * k, this.opacity);
+      },
+      darker: function(k) {
+        k = k == null ? darker : Math.pow(darker, k);
+        return new Hsl(this.h, this.s, this.l * k, this.opacity);
+      },
+      rgb: function() {
+        var h = this.h % 360 + (this.h < 0) * 360,
+            s = isNaN(h) || isNaN(this.s) ? 0 : this.s,
+            l = this.l,
+            m2 = l + (l < 0.5 ? l : 1 - l) * s,
+            m1 = 2 * l - m2;
+        return new Rgb(
+          hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),
+          hsl2rgb(h, m1, m2),
+          hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),
+          this.opacity
+        );
+      },
+      displayable: function() {
+        return (0 <= this.s && this.s <= 1 || isNaN(this.s))
+            && (0 <= this.l && this.l <= 1)
+            && (0 <= this.opacity && this.opacity <= 1);
+      },
+      formatHsl: function() {
+        var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
+        return (a === 1 ? "hsl(" : "hsla(")
+            + (this.h || 0) + ", "
+            + (this.s || 0) * 100 + "%, "
+            + (this.l || 0) * 100 + "%"
+            + (a === 1 ? ")" : ", " + a + ")");
+      }
+    }));
+    
+    /* From FvD 13.37, CSS Color Module Level 3 */
+    function hsl2rgb(h, m1, m2) {
+      return (h < 60 ? m1 + (m2 - m1) * h / 60
+          : h < 180 ? m2
+          : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60
+          : m1) * 255;
+    }
+    
+    const radians$1 = Math.PI / 180;
+    const degrees$2 = 180 / Math.PI;
+    
+    // https://observablehq.com/@mbostock/lab-and-rgb
+    const K = 18,
+        Xn = 0.96422,
+        Yn = 1,
+        Zn = 0.82521,
+        t0$1 = 4 / 29,
+        t1$1 = 6 / 29,
+        t2 = 3 * t1$1 * t1$1,
+        t3 = t1$1 * t1$1 * t1$1;
+    
+    function labConvert(o) {
+      if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);
+      if (o instanceof Hcl) return hcl2lab(o);
+      if (!(o instanceof Rgb)) o = rgbConvert(o);
+      var r = rgb2lrgb(o.r),
+          g = rgb2lrgb(o.g),
+          b = rgb2lrgb(o.b),
+          y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn), x, z;
+      if (r === g && g === b) x = z = y; else {
+        x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
+        z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
+      }
+      return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);
+    }
+    
+    function gray(l, opacity) {
+      return new Lab(l, 0, 0, opacity == null ? 1 : opacity);
+    }
+    
+    function lab$1(l, a, b, opacity) {
+      return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);
+    }
+    
+    function Lab(l, a, b, opacity) {
+      this.l = +l;
+      this.a = +a;
+      this.b = +b;
+      this.opacity = +opacity;
+    }
+    
+    define(Lab, lab$1, extend(Color, {
+      brighter: function(k) {
+        return new Lab(this.l + K * (k == null ? 1 : k), this.a, this.b, this.opacity);
+      },
+      darker: function(k) {
+        return new Lab(this.l - K * (k == null ? 1 : k), this.a, this.b, this.opacity);
+      },
+      rgb: function() {
+        var y = (this.l + 16) / 116,
+            x = isNaN(this.a) ? y : y + this.a / 500,
+            z = isNaN(this.b) ? y : y - this.b / 200;
+        x = Xn * lab2xyz(x);
+        y = Yn * lab2xyz(y);
+        z = Zn * lab2xyz(z);
+        return new Rgb(
+          lrgb2rgb( 3.1338561 * x - 1.6168667 * y - 0.4906146 * z),
+          lrgb2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),
+          lrgb2rgb( 0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
+          this.opacity
+        );
+      }
+    }));
+    
+    function xyz2lab(t) {
+      return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0$1;
+    }
+    
+    function lab2xyz(t) {
+      return t > t1$1 ? t * t * t : t2 * (t - t0$1);
+    }
+    
+    function lrgb2rgb(x) {
+      return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
+    }
+    
+    function rgb2lrgb(x) {
+      return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+    }
+    
+    function hclConvert(o) {
+      if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity);
+      if (!(o instanceof Lab)) o = labConvert(o);
+      if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l && o.l < 100 ? 0 : NaN, o.l, o.opacity);
+      var h = Math.atan2(o.b, o.a) * degrees$2;
+      return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity);
+    }
+    
+    function lch(l, c, h, opacity) {
+      return arguments.length === 1 ? hclConvert(l) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
+    }
+    
+    function hcl$2(h, c, l, opacity) {
+      return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
+    }
+    
+    function Hcl(h, c, l, opacity) {
+      this.h = +h;
+      this.c = +c;
+      this.l = +l;
+      this.opacity = +opacity;
+    }
+    
+    function hcl2lab(o) {
+      if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity);
+      var h = o.h * radians$1;
+      return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);
+    }
+    
+    define(Hcl, hcl$2, extend(Color, {
+      brighter: function(k) {
+        return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity);
+      },
+      darker: function(k) {
+        return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity);
+      },
+      rgb: function() {
+        return hcl2lab(this).rgb();
+      }
+    }));
+    
+    var A = -0.14861,
+        B = +1.78277,
+        C = -0.29227,
+        D = -0.90649,
+        E = +1.97294,
+        ED = E * D,
+        EB = E * B,
+        BC_DA = B * C - D * A;
+    
+    function cubehelixConvert(o) {
+      if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);
+      if (!(o instanceof Rgb)) o = rgbConvert(o);
+      var r = o.r / 255,
+          g = o.g / 255,
+          b = o.b / 255,
+          l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),
+          bl = b - l,
+          k = (E * (g - l) - C * bl) / D,
+          s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1
+          h = s ? Math.atan2(k, bl) * degrees$2 - 120 : NaN;
+      return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);
+    }
+    
+    function cubehelix$3(h, s, l, opacity) {
+      return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);
+    }
+    
+    function Cubehelix(h, s, l, opacity) {
+      this.h = +h;
+      this.s = +s;
+      this.l = +l;
+      this.opacity = +opacity;
+    }
+    
+    define(Cubehelix, cubehelix$3, extend(Color, {
+      brighter: function(k) {
+        k = k == null ? brighter : Math.pow(brighter, k);
+        return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
+      },
+      darker: function(k) {
+        k = k == null ? darker : Math.pow(darker, k);
+        return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
+      },
+      rgb: function() {
+        var h = isNaN(this.h) ? 0 : (this.h + 120) * radians$1,
+            l = +this.l,
+            a = isNaN(this.s) ? 0 : this.s * l * (1 - l),
+            cosh = Math.cos(h),
+            sinh = Math.sin(h);
+        return new Rgb(
+          255 * (l + a * (A * cosh + B * sinh)),
+          255 * (l + a * (C * cosh + D * sinh)),
+          255 * (l + a * (E * cosh)),
+          this.opacity
+        );
+      }
+    }));
+    
+    function basis$1(t1, v0, v1, v2, v3) {
+      var t2 = t1 * t1, t3 = t2 * t1;
+      return ((1 - 3 * t1 + 3 * t2 - t3) * v0
+          + (4 - 6 * t2 + 3 * t3) * v1
+          + (1 + 3 * t1 + 3 * t2 - 3 * t3) * v2
+          + t3 * v3) / 6;
+    }
+    
+    function basis$2(values) {
+      var n = values.length - 1;
+      return function(t) {
+        var i = t <= 0 ? (t = 0) : t >= 1 ? (t = 1, n - 1) : Math.floor(t * n),
+            v1 = values[i],
+            v2 = values[i + 1],
+            v0 = i > 0 ? values[i - 1] : 2 * v1 - v2,
+            v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1;
+        return basis$1((t - i / n) * n, v0, v1, v2, v3);
+      };
+    }
+    
+    function basisClosed$1(values) {
+      var n = values.length;
+      return function(t) {
+        var i = Math.floor(((t %= 1) < 0 ? ++t : t) * n),
+            v0 = values[(i + n - 1) % n],
+            v1 = values[i % n],
+            v2 = values[(i + 1) % n],
+            v3 = values[(i + 2) % n];
+        return basis$1((t - i / n) * n, v0, v1, v2, v3);
+      };
+    }
+    
+    var constant$8 = x => () => x;
+    
+    function linear$2(a, d) {
+      return function(t) {
+        return a + t * d;
+      };
+    }
+    
+    function exponential$1(a, b, y) {
+      return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) {
+        return Math.pow(a + t * b, y);
+      };
+    }
+    
+    function hue$1(a, b) {
+      var d = b - a;
+      return d ? linear$2(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) : constant$8(isNaN(a) ? b : a);
+    }
+    
+    function gamma$1(y) {
+      return (y = +y) === 1 ? nogamma : function(a, b) {
+        return b - a ? exponential$1(a, b, y) : constant$8(isNaN(a) ? b : a);
+      };
+    }
+    
+    function nogamma(a, b) {
+      var d = b - a;
+      return d ? linear$2(a, d) : constant$8(isNaN(a) ? b : a);
+    }
+    
+    var interpolateRgb = (function rgbGamma(y) {
+      var color = gamma$1(y);
+    
+      function rgb$1(start, end) {
+        var r = color((start = rgb(start)).r, (end = rgb(end)).r),
+            g = color(start.g, end.g),
+            b = color(start.b, end.b),
+            opacity = nogamma(start.opacity, end.opacity);
+        return function(t) {
+          start.r = r(t);
+          start.g = g(t);
+          start.b = b(t);
+          start.opacity = opacity(t);
+          return start + "";
+        };
+      }
+    
+      rgb$1.gamma = rgbGamma;
+    
+      return rgb$1;
+    })(1);
+    
+    function rgbSpline(spline) {
+      return function(colors) {
+        var n = colors.length,
+            r = new Array(n),
+            g = new Array(n),
+            b = new Array(n),
+            i, color;
+        for (i = 0; i < n; ++i) {
+          color = rgb(colors[i]);
+          r[i] = color.r || 0;
+          g[i] = color.g || 0;
+          b[i] = color.b || 0;
+        }
+        r = spline(r);
+        g = spline(g);
+        b = spline(b);
+        color.opacity = 1;
+        return function(t) {
+          color.r = r(t);
+          color.g = g(t);
+          color.b = b(t);
+          return color + "";
+        };
+      };
+    }
+    
+    var rgbBasis = rgbSpline(basis$2);
+    var rgbBasisClosed = rgbSpline(basisClosed$1);
+    
+    function numberArray(a, b) {
+      if (!b) b = [];
+      var n = a ? Math.min(b.length, a.length) : 0,
+          c = b.slice(),
+          i;
+      return function(t) {
+        for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t;
+        return c;
+      };
+    }
+    
+    function isNumberArray(x) {
+      return ArrayBuffer.isView(x) && !(x instanceof DataView);
+    }
+    
+    function array$3(a, b) {
+      return (isNumberArray(b) ? numberArray : genericArray)(a, b);
+    }
+    
+    function genericArray(a, b) {
+      var nb = b ? b.length : 0,
+          na = a ? Math.min(nb, a.length) : 0,
+          x = new Array(na),
+          c = new Array(nb),
+          i;
+    
+      for (i = 0; i < na; ++i) x[i] = interpolate$2(a[i], b[i]);
+      for (; i < nb; ++i) c[i] = b[i];
+    
+      return function(t) {
+        for (i = 0; i < na; ++i) c[i] = x[i](t);
+        return c;
+      };
+    }
+    
+    function date$1(a, b) {
+      var d = new Date;
+      return a = +a, b = +b, function(t) {
+        return d.setTime(a * (1 - t) + b * t), d;
+      };
+    }
+    
+    function interpolateNumber(a, b) {
+      return a = +a, b = +b, function(t) {
+        return a * (1 - t) + b * t;
+      };
+    }
+    
+    function object$1(a, b) {
+      var i = {},
+          c = {},
+          k;
+    
+      if (a === null || typeof a !== "object") a = {};
+      if (b === null || typeof b !== "object") b = {};
+    
+      for (k in b) {
+        if (k in a) {
+          i[k] = interpolate$2(a[k], b[k]);
+        } else {
+          c[k] = b[k];
+        }
+      }
+    
+      return function(t) {
+        for (k in i) c[k] = i[k](t);
+        return c;
+      };
+    }
+    
+    var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,
+        reB = new RegExp(reA.source, "g");
+    
+    function zero(b) {
+      return function() {
+        return b;
+      };
+    }
+    
+    function one(b) {
+      return function(t) {
+        return b(t) + "";
+      };
+    }
+    
+    function interpolateString(a, b) {
+      var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b
+          am, // current match in a
+          bm, // current match in b
+          bs, // string preceding current number in b, if any
+          i = -1, // index in s
+          s = [], // string constants and placeholders
+          q = []; // number interpolators
+    
+      // Coerce inputs to strings.
+      a = a + "", b = b + "";
+    
+      // Interpolate pairs of numbers in a & b.
+      while ((am = reA.exec(a))
+          && (bm = reB.exec(b))) {
+        if ((bs = bm.index) > bi) { // a string precedes the next number in b
+          bs = b.slice(bi, bs);
+          if (s[i]) s[i] += bs; // coalesce with previous string
+          else s[++i] = bs;
+        }
+        if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match
+          if (s[i]) s[i] += bm; // coalesce with previous string
+          else s[++i] = bm;
+        } else { // interpolate non-matching numbers
+          s[++i] = null;
+          q.push({i: i, x: interpolateNumber(am, bm)});
+        }
+        bi = reB.lastIndex;
+      }
+    
+      // Add remains of b.
+      if (bi < b.length) {
+        bs = b.slice(bi);
+        if (s[i]) s[i] += bs; // coalesce with previous string
+        else s[++i] = bs;
+      }
+    
+      // Special optimization for only a single match.
+      // Otherwise, interpolate each of the numbers and rejoin the string.
+      return s.length < 2 ? (q[0]
+          ? one(q[0].x)
+          : zero(b))
+          : (b = q.length, function(t) {
+              for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);
+              return s.join("");
+            });
+    }
+    
+    function interpolate$2(a, b) {
+      var t = typeof b, c;
+      return b == null || t === "boolean" ? constant$8(b)
+          : (t === "number" ? interpolateNumber
+          : t === "string" ? ((c = color(b)) ? (b = c, interpolateRgb) : interpolateString)
+          : b instanceof color ? interpolateRgb
+          : b instanceof Date ? date$1
+          : isNumberArray(b) ? numberArray
+          : Array.isArray(b) ? genericArray
+          : typeof b.valueOf !== "function" && typeof b.toString !== "function" || isNaN(b) ? object$1
+          : interpolateNumber)(a, b);
+    }
+    
+    function discrete(range) {
+      var n = range.length;
+      return function(t) {
+        return range[Math.max(0, Math.min(n - 1, Math.floor(t * n)))];
+      };
+    }
+    
+    function hue(a, b) {
+      var i = hue$1(+a, +b);
+      return function(t) {
+        var x = i(t);
+        return x - 360 * Math.floor(x / 360);
+      };
+    }
+    
+    function interpolateRound(a, b) {
+      return a = +a, b = +b, function(t) {
+        return Math.round(a * (1 - t) + b * t);
+      };
+    }
+    
+    var degrees$1 = 180 / Math.PI;
+    
+    var identity$7 = {
+      translateX: 0,
+      translateY: 0,
+      rotate: 0,
+      skewX: 0,
+      scaleX: 1,
+      scaleY: 1
+    };
+    
+    function decompose(a, b, c, d, e, f) {
+      var scaleX, scaleY, skewX;
+      if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;
+      if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;
+      if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;
+      if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;
+      return {
+        translateX: e,
+        translateY: f,
+        rotate: Math.atan2(b, a) * degrees$1,
+        skewX: Math.atan(skewX) * degrees$1,
+        scaleX: scaleX,
+        scaleY: scaleY
+      };
+    }
+    
+    var svgNode;
+    
+    /* eslint-disable no-undef */
+    function parseCss(value) {
+      const m = new (typeof DOMMatrix === "function" ? DOMMatrix : WebKitCSSMatrix)(value + "");
+      return m.isIdentity ? identity$7 : decompose(m.a, m.b, m.c, m.d, m.e, m.f);
+    }
+    
+    function parseSvg(value) {
+      if (value == null) return identity$7;
+      if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
+      svgNode.setAttribute("transform", value);
+      if (!(value = svgNode.transform.baseVal.consolidate())) return identity$7;
+      value = value.matrix;
+      return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
+    }
+    
+    function interpolateTransform(parse, pxComma, pxParen, degParen) {
+    
+      function pop(s) {
+        return s.length ? s.pop() + " " : "";
+      }
+    
+      function translate(xa, ya, xb, yb, s, q) {
+        if (xa !== xb || ya !== yb) {
+          var i = s.push("translate(", null, pxComma, null, pxParen);
+          q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
+        } else if (xb || yb) {
+          s.push("translate(" + xb + pxComma + yb + pxParen);
+        }
+      }
+    
+      function rotate(a, b, s, q) {
+        if (a !== b) {
+          if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path
+          q.push({i: s.push(pop(s) + "rotate(", null, degParen) - 2, x: interpolateNumber(a, b)});
+        } else if (b) {
+          s.push(pop(s) + "rotate(" + b + degParen);
+        }
+      }
+    
+      function skewX(a, b, s, q) {
+        if (a !== b) {
+          q.push({i: s.push(pop(s) + "skewX(", null, degParen) - 2, x: interpolateNumber(a, b)});
+        } else if (b) {
+          s.push(pop(s) + "skewX(" + b + degParen);
+        }
+      }
+    
+      function scale(xa, ya, xb, yb, s, q) {
+        if (xa !== xb || ya !== yb) {
+          var i = s.push(pop(s) + "scale(", null, ",", null, ")");
+          q.push({i: i - 4, x: interpolateNumber(xa, xb)}, {i: i - 2, x: interpolateNumber(ya, yb)});
+        } else if (xb !== 1 || yb !== 1) {
+          s.push(pop(s) + "scale(" + xb + "," + yb + ")");
+        }
+      }
+    
+      return function(a, b) {
+        var s = [], // string constants and placeholders
+            q = []; // number interpolators
+        a = parse(a), b = parse(b);
+        translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q);
+        rotate(a.rotate, b.rotate, s, q);
+        skewX(a.skewX, b.skewX, s, q);
+        scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q);
+        a = b = null; // gc
+        return function(t) {
+          var i = -1, n = q.length, o;
+          while (++i < n) s[(o = q[i]).i] = o.x(t);
+          return s.join("");
+        };
+      };
+    }
+    
+    var interpolateTransformCss = interpolateTransform(parseCss, "px, ", "px)", "deg)");
+    var interpolateTransformSvg = interpolateTransform(parseSvg, ", ", ")", ")");
+    
+    var epsilon2$1 = 1e-12;
+    
+    function cosh(x) {
+      return ((x = Math.exp(x)) + 1 / x) / 2;
+    }
+    
+    function sinh(x) {
+      return ((x = Math.exp(x)) - 1 / x) / 2;
+    }
+    
+    function tanh(x) {
+      return ((x = Math.exp(2 * x)) - 1) / (x + 1);
+    }
+    
+    var interpolateZoom = (function zoomRho(rho, rho2, rho4) {
+    
+      // p0 = [ux0, uy0, w0]
+      // p1 = [ux1, uy1, w1]
+      function zoom(p0, p1) {
+        var ux0 = p0[0], uy0 = p0[1], w0 = p0[2],
+            ux1 = p1[0], uy1 = p1[1], w1 = p1[2],
+            dx = ux1 - ux0,
+            dy = uy1 - uy0,
+            d2 = dx * dx + dy * dy,
+            i,
+            S;
+    
+        // Special case for u0 ≅ u1.
+        if (d2 < epsilon2$1) {
+          S = Math.log(w1 / w0) / rho;
+          i = function(t) {
+            return [
+              ux0 + t * dx,
+              uy0 + t * dy,
+              w0 * Math.exp(rho * t * S)
+            ];
+          };
+        }
+    
+        // General case.
+        else {
+          var d1 = Math.sqrt(d2),
+              b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1),
+              b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1),
+              r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0),
+              r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);
+          S = (r1 - r0) / rho;
+          i = function(t) {
+            var s = t * S,
+                coshr0 = cosh(r0),
+                u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0));
+            return [
+              ux0 + u * dx,
+              uy0 + u * dy,
+              w0 * coshr0 / cosh(rho * s + r0)
+            ];
+          };
+        }
+    
+        i.duration = S * 1000 * rho / Math.SQRT2;
+    
+        return i;
+      }
+    
+      zoom.rho = function(_) {
+        var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2;
+        return zoomRho(_1, _2, _4);
+      };
+    
+      return zoom;
+    })(Math.SQRT2, 2, 4);
+    
+    function hsl(hue) {
+      return function(start, end) {
+        var h = hue((start = hsl$2(start)).h, (end = hsl$2(end)).h),
+            s = nogamma(start.s, end.s),
+            l = nogamma(start.l, end.l),
+            opacity = nogamma(start.opacity, end.opacity);
+        return function(t) {
+          start.h = h(t);
+          start.s = s(t);
+          start.l = l(t);
+          start.opacity = opacity(t);
+          return start + "";
+        };
+      }
+    }
+    
+    var hsl$1 = hsl(hue$1);
+    var hslLong = hsl(nogamma);
+    
+    function lab(start, end) {
+      var l = nogamma((start = lab$1(start)).l, (end = lab$1(end)).l),
+          a = nogamma(start.a, end.a),
+          b = nogamma(start.b, end.b),
+          opacity = nogamma(start.opacity, end.opacity);
+      return function(t) {
+        start.l = l(t);
+        start.a = a(t);
+        start.b = b(t);
+        start.opacity = opacity(t);
+        return start + "";
+      };
+    }
+    
+    function hcl(hue) {
+      return function(start, end) {
+        var h = hue((start = hcl$2(start)).h, (end = hcl$2(end)).h),
+            c = nogamma(start.c, end.c),
+            l = nogamma(start.l, end.l),
+            opacity = nogamma(start.opacity, end.opacity);
+        return function(t) {
+          start.h = h(t);
+          start.c = c(t);
+          start.l = l(t);
+          start.opacity = opacity(t);
+          return start + "";
+        };
+      }
+    }
+    
+    var hcl$1 = hcl(hue$1);
+    var hclLong = hcl(nogamma);
+    
+    function cubehelix$1(hue) {
+      return (function cubehelixGamma(y) {
+        y = +y;
+    
+        function cubehelix(start, end) {
+          var h = hue((start = cubehelix$3(start)).h, (end = cubehelix$3(end)).h),
+              s = nogamma(start.s, end.s),
+              l = nogamma(start.l, end.l),
+              opacity = nogamma(start.opacity, end.opacity);
+          return function(t) {
+            start.h = h(t);
+            start.s = s(t);
+            start.l = l(Math.pow(t, y));
+            start.opacity = opacity(t);
+            return start + "";
+          };
+        }
+    
+        cubehelix.gamma = cubehelixGamma;
+    
+        return cubehelix;
+      })(1);
+    }
+    
+    var cubehelix$2 = cubehelix$1(hue$1);
+    var cubehelixLong = cubehelix$1(nogamma);
+    
+    function piecewise(interpolate, values) {
+      if (values === undefined) values = interpolate, interpolate = interpolate$2;
+      var i = 0, n = values.length - 1, v = values[0], I = new Array(n < 0 ? 0 : n);
+      while (i < n) I[i] = interpolate(v, v = values[++i]);
+      return function(t) {
+        var i = Math.max(0, Math.min(n - 1, Math.floor(t *= n)));
+        return I[i](t - i);
+      };
+    }
+    
+    function quantize$1(interpolator, n) {
+      var samples = new Array(n);
+      for (var i = 0; i < n; ++i) samples[i] = interpolator(i / (n - 1));
+      return samples;
+    }
+    
+    var frame = 0, // is an animation frame pending?
+        timeout$1 = 0, // is a timeout pending?
+        interval$1 = 0, // are any timers active?
+        pokeDelay = 1000, // how frequently we check for clock skew
+        taskHead,
+        taskTail,
+        clockLast = 0,
+        clockNow = 0,
+        clockSkew = 0,
+        clock = typeof performance === "object" && performance.now ? performance : Date,
+        setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };
+    
+    function now() {
+      return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);
+    }
+    
+    function clearNow() {
+      clockNow = 0;
+    }
+    
+    function Timer() {
+      this._call =
+      this._time =
+      this._next = null;
+    }
+    
+    Timer.prototype = timer.prototype = {
+      constructor: Timer,
+      restart: function(callback, delay, time) {
+        if (typeof callback !== "function") throw new TypeError("callback is not a function");
+        time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);
+        if (!this._next && taskTail !== this) {
+          if (taskTail) taskTail._next = this;
+          else taskHead = this;
+          taskTail = this;
+        }
+        this._call = callback;
+        this._time = time;
+        sleep();
+      },
+      stop: function() {
+        if (this._call) {
+          this._call = null;
+          this._time = Infinity;
+          sleep();
+        }
+      }
+    };
+    
+    function timer(callback, delay, time) {
+      var t = new Timer;
+      t.restart(callback, delay, time);
+      return t;
+    }
+    
+    function timerFlush() {
+      now(); // Get the current time, if not already set.
+      ++frame; // Pretend we’ve set an alarm, if we haven’t already.
+      var t = taskHead, e;
+      while (t) {
+        if ((e = clockNow - t._time) >= 0) t._call.call(null, e);
+        t = t._next;
+      }
+      --frame;
+    }
+    
+    function wake() {
+      clockNow = (clockLast = clock.now()) + clockSkew;
+      frame = timeout$1 = 0;
+      try {
+        timerFlush();
+      } finally {
+        frame = 0;
+        nap();
+        clockNow = 0;
+      }
+    }
+    
+    function poke() {
+      var now = clock.now(), delay = now - clockLast;
+      if (delay > pokeDelay) clockSkew -= delay, clockLast = now;
+    }
+    
+    function nap() {
+      var t0, t1 = taskHead, t2, time = Infinity;
+      while (t1) {
+        if (t1._call) {
+          if (time > t1._time) time = t1._time;
+          t0 = t1, t1 = t1._next;
+        } else {
+          t2 = t1._next, t1._next = null;
+          t1 = t0 ? t0._next = t2 : taskHead = t2;
+        }
+      }
+      taskTail = t0;
+      sleep(time);
+    }
+    
+    function sleep(time) {
+      if (frame) return; // Soonest alarm already set, or will be.
+      if (timeout$1) timeout$1 = clearTimeout(timeout$1);
+      var delay = time - clockNow; // Strictly less than if we recomputed clockNow.
+      if (delay > 24) {
+        if (time < Infinity) timeout$1 = setTimeout(wake, time - clock.now() - clockSkew);
+        if (interval$1) interval$1 = clearInterval(interval$1);
+      } else {
+        if (!interval$1) clockLast = clock.now(), interval$1 = setInterval(poke, pokeDelay);
+        frame = 1, setFrame(wake);
+      }
+    }
+    
+    function timeout(callback, delay, time) {
+      var t = new Timer;
+      delay = delay == null ? 0 : +delay;
+      t.restart(elapsed => {
+        t.stop();
+        callback(elapsed + delay);
+      }, delay, time);
+      return t;
+    }
+    
+    function interval(callback, delay, time) {
+      var t = new Timer, total = delay;
+      if (delay == null) return t.restart(callback, delay, time), t;
+      t._restart = t.restart;
+      t.restart = function(callback, delay, time) {
+        delay = +delay, time = time == null ? now() : +time;
+        t._restart(function tick(elapsed) {
+          elapsed += total;
+          t._restart(tick, total += delay, time);
+          callback(elapsed);
+        }, delay, time);
+      };
+      t.restart(callback, delay, time);
+      return t;
+    }
+    
+    var emptyOn = dispatch("start", "end", "cancel", "interrupt");
+    var emptyTween = [];
+    
+    var CREATED = 0;
+    var SCHEDULED = 1;
+    var STARTING = 2;
+    var STARTED = 3;
+    var RUNNING = 4;
+    var ENDING = 5;
+    var ENDED = 6;
+    
+    function schedule(node, name, id, index, group, timing) {
+      var schedules = node.__transition;
+      if (!schedules) node.__transition = {};
+      else if (id in schedules) return;
+      create(node, id, {
+        name: name,
+        index: index, // For context during callback.
+        group: group, // For context during callback.
+        on: emptyOn,
+        tween: emptyTween,
+        time: timing.time,
+        delay: timing.delay,
+        duration: timing.duration,
+        ease: timing.ease,
+        timer: null,
+        state: CREATED
+      });
+    }
+    
+    function init(node, id) {
+      var schedule = get(node, id);
+      if (schedule.state > CREATED) throw new Error("too late; already scheduled");
+      return schedule;
+    }
+    
+    function set(node, id) {
+      var schedule = get(node, id);
+      if (schedule.state > STARTED) throw new Error("too late; already running");
+      return schedule;
+    }
+    
+    function get(node, id) {
+      var schedule = node.__transition;
+      if (!schedule || !(schedule = schedule[id])) throw new Error("transition not found");
+      return schedule;
+    }
+    
+    function create(node, id, self) {
+      var schedules = node.__transition,
+          tween;
+    
+      // Initialize the self timer when the transition is created.
+      // Note the actual delay is not known until the first callback!
+      schedules[id] = self;
+      self.timer = timer(schedule, 0, self.time);
+    
+      function schedule(elapsed) {
+        self.state = SCHEDULED;
+        self.timer.restart(start, self.delay, self.time);
+    
+        // If the elapsed delay is less than our first sleep, start immediately.
+        if (self.delay <= elapsed) start(elapsed - self.delay);
+      }
+    
+      function start(elapsed) {
+        var i, j, n, o;
+    
+        // If the state is not SCHEDULED, then we previously errored on start.
+        if (self.state !== SCHEDULED) return stop();
+    
+        for (i in schedules) {
+          o = schedules[i];
+          if (o.name !== self.name) continue;
+    
+          // While this element already has a starting transition during this frame,
+          // defer starting an interrupting transition until that transition has a
+          // chance to tick (and possibly end); see d3/d3-transition#54!
+          if (o.state === STARTED) return timeout(start);
+    
+          // Interrupt the active transition, if any.
+          if (o.state === RUNNING) {
+            o.state = ENDED;
+            o.timer.stop();
+            o.on.call("interrupt", node, node.__data__, o.index, o.group);
+            delete schedules[i];
+          }
+    
+          // Cancel any pre-empted transitions.
+          else if (+i < id) {
+            o.state = ENDED;
+            o.timer.stop();
+            o.on.call("cancel", node, node.__data__, o.index, o.group);
+            delete schedules[i];
+          }
+        }
+    
+        // Defer the first tick to end of the current frame; see d3/d3#1576.
+        // Note the transition may be canceled after start and before the first tick!
+        // Note this must be scheduled before the start event; see d3/d3-transition#16!
+        // Assuming this is successful, subsequent callbacks go straight to tick.
+        timeout(function() {
+          if (self.state === STARTED) {
+            self.state = RUNNING;
+            self.timer.restart(tick, self.delay, self.time);
+            tick(elapsed);
+          }
+        });
+    
+        // Dispatch the start event.
+        // Note this must be done before the tween are initialized.
+        self.state = STARTING;
+        self.on.call("start", node, node.__data__, self.index, self.group);
+        if (self.state !== STARTING) return; // interrupted
+        self.state = STARTED;
+    
+        // Initialize the tween, deleting null tween.
+        tween = new Array(n = self.tween.length);
+        for (i = 0, j = -1; i < n; ++i) {
+          if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) {
+            tween[++j] = o;
+          }
+        }
+        tween.length = j + 1;
+      }
+    
+      function tick(elapsed) {
+        var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),
+            i = -1,
+            n = tween.length;
+    
+        while (++i < n) {
+          tween[i].call(node, t);
+        }
+    
+        // Dispatch the end event.
+        if (self.state === ENDING) {
+          self.on.call("end", node, node.__data__, self.index, self.group);
+          stop();
+        }
+      }
+    
+      function stop() {
+        self.state = ENDED;
+        self.timer.stop();
+        delete schedules[id];
+        for (var i in schedules) return; // eslint-disable-line no-unused-vars
+        delete node.__transition;
+      }
+    }
+    
+    function interrupt(node, name) {
+      var schedules = node.__transition,
+          schedule,
+          active,
+          empty = true,
+          i;
+    
+      if (!schedules) return;
+    
+      name = name == null ? null : name + "";
+    
+      for (i in schedules) {
+        if ((schedule = schedules[i]).name !== name) { empty = false; continue; }
+        active = schedule.state > STARTING && schedule.state < ENDING;
+        schedule.state = ENDED;
+        schedule.timer.stop();
+        schedule.on.call(active ? "interrupt" : "cancel", node, node.__data__, schedule.index, schedule.group);
+        delete schedules[i];
+      }
+    
+      if (empty) delete node.__transition;
+    }
+    
+    function selection_interrupt(name) {
+      return this.each(function() {
+        interrupt(this, name);
+      });
+    }
+    
+    function tweenRemove(id, name) {
+      var tween0, tween1;
+      return function() {
+        var schedule = set(this, id),
+            tween = schedule.tween;
+    
+        // If this node shared tween with the previous node,
+        // just assign the updated shared tween and we’re done!
+        // Otherwise, copy-on-write.
+        if (tween !== tween0) {
+          tween1 = tween0 = tween;
+          for (var i = 0, n = tween1.length; i < n; ++i) {
+            if (tween1[i].name === name) {
+              tween1 = tween1.slice();
+              tween1.splice(i, 1);
+              break;
+            }
+          }
+        }
+    
+        schedule.tween = tween1;
+      };
+    }
+    
+    function tweenFunction(id, name, value) {
+      var tween0, tween1;
+      if (typeof value !== "function") throw new Error;
+      return function() {
+        var schedule = set(this, id),
+            tween = schedule.tween;
+    
+        // If this node shared tween with the previous node,
+        // just assign the updated shared tween and we’re done!
+        // Otherwise, copy-on-write.
+        if (tween !== tween0) {
+          tween1 = (tween0 = tween).slice();
+          for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) {
+            if (tween1[i].name === name) {
+              tween1[i] = t;
+              break;
+            }
+          }
+          if (i === n) tween1.push(t);
+        }
+    
+        schedule.tween = tween1;
+      };
+    }
+    
+    function transition_tween(name, value) {
+      var id = this._id;
+    
+      name += "";
+    
+      if (arguments.length < 2) {
+        var tween = get(this.node(), id).tween;
+        for (var i = 0, n = tween.length, t; i < n; ++i) {
+          if ((t = tween[i]).name === name) {
+            return t.value;
+          }
+        }
+        return null;
+      }
+    
+      return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value));
+    }
+    
+    function tweenValue(transition, name, value) {
+      var id = transition._id;
+    
+      transition.each(function() {
+        var schedule = set(this, id);
+        (schedule.value || (schedule.value = {}))[name] = value.apply(this, arguments);
+      });
+    
+      return function(node) {
+        return get(node, id).value[name];
+      };
+    }
+    
+    function interpolate$1(a, b) {
+      var c;
+      return (typeof b === "number" ? interpolateNumber
+          : b instanceof color ? interpolateRgb
+          : (c = color(b)) ? (b = c, interpolateRgb)
+          : interpolateString)(a, b);
+    }
+    
+    function attrRemove(name) {
+      return function() {
+        this.removeAttribute(name);
+      };
+    }
+    
+    function attrRemoveNS(fullname) {
+      return function() {
+        this.removeAttributeNS(fullname.space, fullname.local);
+      };
+    }
+    
+    function attrConstant(name, interpolate, value1) {
+      var string00,
+          string1 = value1 + "",
+          interpolate0;
+      return function() {
+        var string0 = this.getAttribute(name);
+        return string0 === string1 ? null
+            : string0 === string00 ? interpolate0
+            : interpolate0 = interpolate(string00 = string0, value1);
+      };
+    }
+    
+    function attrConstantNS(fullname, interpolate, value1) {
+      var string00,
+          string1 = value1 + "",
+          interpolate0;
+      return function() {
+        var string0 = this.getAttributeNS(fullname.space, fullname.local);
+        return string0 === string1 ? null
+            : string0 === string00 ? interpolate0
+            : interpolate0 = interpolate(string00 = string0, value1);
+      };
+    }
+    
+    function attrFunction(name, interpolate, value) {
+      var string00,
+          string10,
+          interpolate0;
+      return function() {
+        var string0, value1 = value(this), string1;
+        if (value1 == null) return void this.removeAttribute(name);
+        string0 = this.getAttribute(name);
+        string1 = value1 + "";
+        return string0 === string1 ? null
+            : string0 === string00 && string1 === string10 ? interpolate0
+            : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
+      };
+    }
+    
+    function attrFunctionNS(fullname, interpolate, value) {
+      var string00,
+          string10,
+          interpolate0;
+      return function() {
+        var string0, value1 = value(this), string1;
+        if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
+        string0 = this.getAttributeNS(fullname.space, fullname.local);
+        string1 = value1 + "";
+        return string0 === string1 ? null
+            : string0 === string00 && string1 === string10 ? interpolate0
+            : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
+      };
+    }
+    
+    function transition_attr(name, value) {
+      var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate$1;
+      return this.attrTween(name, typeof value === "function"
+          ? (fullname.local ? attrFunctionNS : attrFunction)(fullname, i, tweenValue(this, "attr." + name, value))
+          : value == null ? (fullname.local ? attrRemoveNS : attrRemove)(fullname)
+          : (fullname.local ? attrConstantNS : attrConstant)(fullname, i, value));
+    }
+    
+    function attrInterpolate(name, i) {
+      return function(t) {
+        this.setAttribute(name, i.call(this, t));
+      };
+    }
+    
+    function attrInterpolateNS(fullname, i) {
+      return function(t) {
+        this.setAttributeNS(fullname.space, fullname.local, i.call(this, t));
+      };
+    }
+    
+    function attrTweenNS(fullname, value) {
+      var t0, i0;
+      function tween() {
+        var i = value.apply(this, arguments);
+        if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);
+        return t0;
+      }
+      tween._value = value;
+      return tween;
+    }
+    
+    function attrTween(name, value) {
+      var t0, i0;
+      function tween() {
+        var i = value.apply(this, arguments);
+        if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);
+        return t0;
+      }
+      tween._value = value;
+      return tween;
+    }
+    
+    function transition_attrTween(name, value) {
+      var key = "attr." + name;
+      if (arguments.length < 2) return (key = this.tween(key)) && key._value;
+      if (value == null) return this.tween(key, null);
+      if (typeof value !== "function") throw new Error;
+      var fullname = namespace(name);
+      return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value));
+    }
+    
+    function delayFunction(id, value) {
+      return function() {
+        init(this, id).delay = +value.apply(this, arguments);
+      };
+    }
+    
+    function delayConstant(id, value) {
+      return value = +value, function() {
+        init(this, id).delay = value;
+      };
+    }
+    
+    function transition_delay(value) {
+      var id = this._id;
+    
+      return arguments.length
+          ? this.each((typeof value === "function"
+              ? delayFunction
+              : delayConstant)(id, value))
+          : get(this.node(), id).delay;
+    }
+    
+    function durationFunction(id, value) {
+      return function() {
+        set(this, id).duration = +value.apply(this, arguments);
+      };
+    }
+    
+    function durationConstant(id, value) {
+      return value = +value, function() {
+        set(this, id).duration = value;
+      };
+    }
+    
+    function transition_duration(value) {
+      var id = this._id;
+    
+      return arguments.length
+          ? this.each((typeof value === "function"
+              ? durationFunction
+              : durationConstant)(id, value))
+          : get(this.node(), id).duration;
+    }
+    
+    function easeConstant(id, value) {
+      if (typeof value !== "function") throw new Error;
+      return function() {
+        set(this, id).ease = value;
+      };
+    }
+    
+    function transition_ease(value) {
+      var id = this._id;
+    
+      return arguments.length
+          ? this.each(easeConstant(id, value))
+          : get(this.node(), id).ease;
+    }
+    
+    function easeVarying(id, value) {
+      return function() {
+        var v = value.apply(this, arguments);
+        if (typeof v !== "function") throw new Error;
+        set(this, id).ease = v;
+      };
+    }
+    
+    function transition_easeVarying(value) {
+      if (typeof value !== "function") throw new Error;
+      return this.each(easeVarying(this._id, value));
+    }
+    
+    function transition_filter(match) {
+      if (typeof match !== "function") match = matcher(match);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
+          if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
+            subgroup.push(node);
+          }
+        }
+      }
+    
+      return new Transition(subgroups, this._parents, this._name, this._id);
+    }
+    
+    function transition_merge(transition) {
+      if (transition._id !== this._id) throw new Error;
+    
+      for (var groups0 = this._groups, groups1 = transition._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
+        for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
+          if (node = group0[i] || group1[i]) {
+            merge[i] = node;
+          }
+        }
+      }
+    
+      for (; j < m0; ++j) {
+        merges[j] = groups0[j];
+      }
+    
+      return new Transition(merges, this._parents, this._name, this._id);
+    }
+    
+    function start(name) {
+      return (name + "").trim().split(/^|\s+/).every(function(t) {
+        var i = t.indexOf(".");
+        if (i >= 0) t = t.slice(0, i);
+        return !t || t === "start";
+      });
+    }
+    
+    function onFunction(id, name, listener) {
+      var on0, on1, sit = start(name) ? init : set;
+      return function() {
+        var schedule = sit(this, id),
+            on = schedule.on;
+    
+        // If this node shared a dispatch with the previous node,
+        // just assign the updated shared dispatch and we’re done!
+        // Otherwise, copy-on-write.
+        if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener);
+    
+        schedule.on = on1;
+      };
+    }
+    
+    function transition_on(name, listener) {
+      var id = this._id;
+    
+      return arguments.length < 2
+          ? get(this.node(), id).on.on(name)
+          : this.each(onFunction(id, name, listener));
+    }
+    
+    function removeFunction(id) {
+      return function() {
+        var parent = this.parentNode;
+        for (var i in this.__transition) if (+i !== id) return;
+        if (parent) parent.removeChild(this);
+      };
+    }
+    
+    function transition_remove() {
+      return this.on("end.remove", removeFunction(this._id));
+    }
+    
+    function transition_select(select) {
+      var name = this._name,
+          id = this._id;
+    
+      if (typeof select !== "function") select = selector(select);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
+          if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
+            if ("__data__" in node) subnode.__data__ = node.__data__;
+            subgroup[i] = subnode;
+            schedule(subgroup[i], name, id, i, subgroup, get(node, id));
+          }
+        }
+      }
+    
+      return new Transition(subgroups, this._parents, name, id);
+    }
+    
+    function transition_selectAll(select) {
+      var name = this._name,
+          id = this._id;
+    
+      if (typeof select !== "function") select = selectorAll(select);
+    
+      for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+          if (node = group[i]) {
+            for (var children = select.call(node, node.__data__, i, group), child, inherit = get(node, id), k = 0, l = children.length; k < l; ++k) {
+              if (child = children[k]) {
+                schedule(child, name, id, k, children, inherit);
+              }
+            }
+            subgroups.push(children);
+            parents.push(node);
+          }
+        }
+      }
+    
+      return new Transition(subgroups, parents, name, id);
+    }
+    
+    var Selection = selection.prototype.constructor;
+    
+    function transition_selection() {
+      return new Selection(this._groups, this._parents);
+    }
+    
+    function styleNull(name, interpolate) {
+      var string00,
+          string10,
+          interpolate0;
+      return function() {
+        var string0 = styleValue(this, name),
+            string1 = (this.style.removeProperty(name), styleValue(this, name));
+        return string0 === string1 ? null
+            : string0 === string00 && string1 === string10 ? interpolate0
+            : interpolate0 = interpolate(string00 = string0, string10 = string1);
+      };
+    }
+    
+    function styleRemove(name) {
+      return function() {
+        this.style.removeProperty(name);
+      };
+    }
+    
+    function styleConstant(name, interpolate, value1) {
+      var string00,
+          string1 = value1 + "",
+          interpolate0;
+      return function() {
+        var string0 = styleValue(this, name);
+        return string0 === string1 ? null
+            : string0 === string00 ? interpolate0
+            : interpolate0 = interpolate(string00 = string0, value1);
+      };
+    }
+    
+    function styleFunction(name, interpolate, value) {
+      var string00,
+          string10,
+          interpolate0;
+      return function() {
+        var string0 = styleValue(this, name),
+            value1 = value(this),
+            string1 = value1 + "";
+        if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
+        return string0 === string1 ? null
+            : string0 === string00 && string1 === string10 ? interpolate0
+            : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
+      };
+    }
+    
+    function styleMaybeRemove(id, name) {
+      var on0, on1, listener0, key = "style." + name, event = "end." + key, remove;
+      return function() {
+        var schedule = set(this, id),
+            on = schedule.on,
+            listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;
+    
+        // If this node shared a dispatch with the previous node,
+        // just assign the updated shared dispatch and we’re done!
+        // Otherwise, copy-on-write.
+        if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);
+    
+        schedule.on = on1;
+      };
+    }
+    
+    function transition_style(name, value, priority) {
+      var i = (name += "") === "transform" ? interpolateTransformCss : interpolate$1;
+      return value == null ? this
+          .styleTween(name, styleNull(name, i))
+          .on("end.style." + name, styleRemove(name))
+        : typeof value === "function" ? this
+          .styleTween(name, styleFunction(name, i, tweenValue(this, "style." + name, value)))
+          .each(styleMaybeRemove(this._id, name))
+        : this
+          .styleTween(name, styleConstant(name, i, value), priority)
+          .on("end.style." + name, null);
+    }
+    
+    function styleInterpolate(name, i, priority) {
+      return function(t) {
+        this.style.setProperty(name, i.call(this, t), priority);
+      };
+    }
+    
+    function styleTween(name, value, priority) {
+      var t, i0;
+      function tween() {
+        var i = value.apply(this, arguments);
+        if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);
+        return t;
+      }
+      tween._value = value;
+      return tween;
+    }
+    
+    function transition_styleTween(name, value, priority) {
+      var key = "style." + (name += "");
+      if (arguments.length < 2) return (key = this.tween(key)) && key._value;
+      if (value == null) return this.tween(key, null);
+      if (typeof value !== "function") throw new Error;
+      return this.tween(key, styleTween(name, value, priority == null ? "" : priority));
+    }
+    
+    function textConstant(value) {
+      return function() {
+        this.textContent = value;
+      };
+    }
+    
+    function textFunction(value) {
+      return function() {
+        var value1 = value(this);
+        this.textContent = value1 == null ? "" : value1;
+      };
+    }
+    
+    function transition_text(value) {
+      return this.tween("text", typeof value === "function"
+          ? textFunction(tweenValue(this, "text", value))
+          : textConstant(value == null ? "" : value + ""));
+    }
+    
+    function textInterpolate(i) {
+      return function(t) {
+        this.textContent = i.call(this, t);
+      };
+    }
+    
+    function textTween(value) {
+      var t0, i0;
+      function tween() {
+        var i = value.apply(this, arguments);
+        if (i !== i0) t0 = (i0 = i) && textInterpolate(i);
+        return t0;
+      }
+      tween._value = value;
+      return tween;
+    }
+    
+    function transition_textTween(value) {
+      var key = "text";
+      if (arguments.length < 1) return (key = this.tween(key)) && key._value;
+      if (value == null) return this.tween(key, null);
+      if (typeof value !== "function") throw new Error;
+      return this.tween(key, textTween(value));
+    }
+    
+    function transition_transition() {
+      var name = this._name,
+          id0 = this._id,
+          id1 = newId();
+    
+      for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+          if (node = group[i]) {
+            var inherit = get(node, id0);
+            schedule(node, name, id1, i, group, {
+              time: inherit.time + inherit.delay + inherit.duration,
+              delay: 0,
+              duration: inherit.duration,
+              ease: inherit.ease
+            });
+          }
+        }
+      }
+    
+      return new Transition(groups, this._parents, name, id1);
+    }
+    
+    function transition_end() {
+      var on0, on1, that = this, id = that._id, size = that.size();
+      return new Promise(function(resolve, reject) {
+        var cancel = {value: reject},
+            end = {value: function() { if (--size === 0) resolve(); }};
+    
+        that.each(function() {
+          var schedule = set(this, id),
+              on = schedule.on;
+    
+          // If this node shared a dispatch with the previous node,
+          // just assign the updated shared dispatch and we’re done!
+          // Otherwise, copy-on-write.
+          if (on !== on0) {
+            on1 = (on0 = on).copy();
+            on1._.cancel.push(cancel);
+            on1._.interrupt.push(cancel);
+            on1._.end.push(end);
+          }
+    
+          schedule.on = on1;
+        });
+    
+        // The selection was empty, resolve end immediately
+        if (size === 0) resolve();
+      });
+    }
+    
+    var id = 0;
+    
+    function Transition(groups, parents, name, id) {
+      this._groups = groups;
+      this._parents = parents;
+      this._name = name;
+      this._id = id;
+    }
+    
+    function transition(name) {
+      return selection().transition(name);
+    }
+    
+    function newId() {
+      return ++id;
+    }
+    
+    var selection_prototype = selection.prototype;
+    
+    Transition.prototype = transition.prototype = {
+      constructor: Transition,
+      select: transition_select,
+      selectAll: transition_selectAll,
+      filter: transition_filter,
+      merge: transition_merge,
+      selection: transition_selection,
+      transition: transition_transition,
+      call: selection_prototype.call,
+      nodes: selection_prototype.nodes,
+      node: selection_prototype.node,
+      size: selection_prototype.size,
+      empty: selection_prototype.empty,
+      each: selection_prototype.each,
+      on: transition_on,
+      attr: transition_attr,
+      attrTween: transition_attrTween,
+      style: transition_style,
+      styleTween: transition_styleTween,
+      text: transition_text,
+      textTween: transition_textTween,
+      remove: transition_remove,
+      tween: transition_tween,
+      delay: transition_delay,
+      duration: transition_duration,
+      ease: transition_ease,
+      easeVarying: transition_easeVarying,
+      end: transition_end,
+      [Symbol.iterator]: selection_prototype[Symbol.iterator]
+    };
+    
+    const linear$1 = t => +t;
+    
+    function quadIn(t) {
+      return t * t;
+    }
+    
+    function quadOut(t) {
+      return t * (2 - t);
+    }
+    
+    function quadInOut(t) {
+      return ((t *= 2) <= 1 ? t * t : --t * (2 - t) + 1) / 2;
+    }
+    
+    function cubicIn(t) {
+      return t * t * t;
+    }
+    
+    function cubicOut(t) {
+      return --t * t * t + 1;
+    }
+    
+    function cubicInOut(t) {
+      return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
+    }
+    
+    var exponent$1 = 3;
+    
+    var polyIn = (function custom(e) {
+      e = +e;
+    
+      function polyIn(t) {
+        return Math.pow(t, e);
+      }
+    
+      polyIn.exponent = custom;
+    
+      return polyIn;
+    })(exponent$1);
+    
+    var polyOut = (function custom(e) {
+      e = +e;
+    
+      function polyOut(t) {
+        return 1 - Math.pow(1 - t, e);
+      }
+    
+      polyOut.exponent = custom;
+    
+      return polyOut;
+    })(exponent$1);
+    
+    var polyInOut = (function custom(e) {
+      e = +e;
+    
+      function polyInOut(t) {
+        return ((t *= 2) <= 1 ? Math.pow(t, e) : 2 - Math.pow(2 - t, e)) / 2;
+      }
+    
+      polyInOut.exponent = custom;
+    
+      return polyInOut;
+    })(exponent$1);
+    
+    var pi$4 = Math.PI,
+        halfPi$3 = pi$4 / 2;
+    
+    function sinIn(t) {
+      return (+t === 1) ? 1 : 1 - Math.cos(t * halfPi$3);
+    }
+    
+    function sinOut(t) {
+      return Math.sin(t * halfPi$3);
+    }
+    
+    function sinInOut(t) {
+      return (1 - Math.cos(pi$4 * t)) / 2;
+    }
+    
+    // tpmt is two power minus ten times t scaled to [0,1]
+    function tpmt(x) {
+      return (Math.pow(2, -10 * x) - 0.0009765625) * 1.0009775171065494;
+    }
+    
+    function expIn(t) {
+      return tpmt(1 - +t);
+    }
+    
+    function expOut(t) {
+      return 1 - tpmt(t);
+    }
+    
+    function expInOut(t) {
+      return ((t *= 2) <= 1 ? tpmt(1 - t) : 2 - tpmt(t - 1)) / 2;
+    }
+    
+    function circleIn(t) {
+      return 1 - Math.sqrt(1 - t * t);
+    }
+    
+    function circleOut(t) {
+      return Math.sqrt(1 - --t * t);
+    }
+    
+    function circleInOut(t) {
+      return ((t *= 2) <= 1 ? 1 - Math.sqrt(1 - t * t) : Math.sqrt(1 - (t -= 2) * t) + 1) / 2;
+    }
+    
+    var b1 = 4 / 11,
+        b2 = 6 / 11,
+        b3 = 8 / 11,
+        b4 = 3 / 4,
+        b5 = 9 / 11,
+        b6 = 10 / 11,
+        b7 = 15 / 16,
+        b8 = 21 / 22,
+        b9 = 63 / 64,
+        b0 = 1 / b1 / b1;
+    
+    function bounceIn(t) {
+      return 1 - bounceOut(1 - t);
+    }
+    
+    function bounceOut(t) {
+      return (t = +t) < b1 ? b0 * t * t : t < b3 ? b0 * (t -= b2) * t + b4 : t < b6 ? b0 * (t -= b5) * t + b7 : b0 * (t -= b8) * t + b9;
+    }
+    
+    function bounceInOut(t) {
+      return ((t *= 2) <= 1 ? 1 - bounceOut(1 - t) : bounceOut(t - 1) + 1) / 2;
+    }
+    
+    var overshoot = 1.70158;
+    
+    var backIn = (function custom(s) {
+      s = +s;
+    
+      function backIn(t) {
+        return (t = +t) * t * (s * (t - 1) + t);
+      }
+    
+      backIn.overshoot = custom;
+    
+      return backIn;
+    })(overshoot);
+    
+    var backOut = (function custom(s) {
+      s = +s;
+    
+      function backOut(t) {
+        return --t * t * ((t + 1) * s + t) + 1;
+      }
+    
+      backOut.overshoot = custom;
+    
+      return backOut;
+    })(overshoot);
+    
+    var backInOut = (function custom(s) {
+      s = +s;
+    
+      function backInOut(t) {
+        return ((t *= 2) < 1 ? t * t * ((s + 1) * t - s) : (t -= 2) * t * ((s + 1) * t + s) + 2) / 2;
+      }
+    
+      backInOut.overshoot = custom;
+    
+      return backInOut;
+    })(overshoot);
+    
+    var tau$5 = 2 * Math.PI,
+        amplitude = 1,
+        period = 0.3;
+    
+    var elasticIn = (function custom(a, p) {
+      var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau$5);
+    
+      function elasticIn(t) {
+        return a * tpmt(-(--t)) * Math.sin((s - t) / p);
+      }
+    
+      elasticIn.amplitude = function(a) { return custom(a, p * tau$5); };
+      elasticIn.period = function(p) { return custom(a, p); };
+    
+      return elasticIn;
+    })(amplitude, period);
+    
+    var elasticOut = (function custom(a, p) {
+      var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau$5);
+    
+      function elasticOut(t) {
+        return 1 - a * tpmt(t = +t) * Math.sin((t + s) / p);
+      }
+    
+      elasticOut.amplitude = function(a) { return custom(a, p * tau$5); };
+      elasticOut.period = function(p) { return custom(a, p); };
+    
+      return elasticOut;
+    })(amplitude, period);
+    
+    var elasticInOut = (function custom(a, p) {
+      var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau$5);
+    
+      function elasticInOut(t) {
+        return ((t = t * 2 - 1) < 0
+            ? a * tpmt(-t) * Math.sin((s - t) / p)
+            : 2 - a * tpmt(t) * Math.sin((s + t) / p)) / 2;
+      }
+    
+      elasticInOut.amplitude = function(a) { return custom(a, p * tau$5); };
+      elasticInOut.period = function(p) { return custom(a, p); };
+    
+      return elasticInOut;
+    })(amplitude, period);
+    
+    var defaultTiming = {
+      time: null, // Set on use.
+      delay: 0,
+      duration: 250,
+      ease: cubicInOut
+    };
+    
+    function inherit(node, id) {
+      var timing;
+      while (!(timing = node.__transition) || !(timing = timing[id])) {
+        if (!(node = node.parentNode)) {
+          throw new Error(`transition ${id} not found`);
+        }
+      }
+      return timing;
+    }
+    
+    function selection_transition(name) {
+      var id,
+          timing;
+    
+      if (name instanceof Transition) {
+        id = name._id, name = name._name;
+      } else {
+        id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + "";
+      }
+    
+      for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {
+        for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
+          if (node = group[i]) {
+            schedule(node, name, id, i, group, timing || inherit(node, id));
+          }
+        }
+      }
+    
+      return new Transition(groups, this._parents, name, id);
+    }
+    
+    selection.prototype.interrupt = selection_interrupt;
+    selection.prototype.transition = selection_transition;
+    
+    var root = [null];
+    
+    function active(node, name) {
+      var schedules = node.__transition,
+          schedule,
+          i;
+    
+      if (schedules) {
+        name = name == null ? null : name + "";
+        for (i in schedules) {
+          if ((schedule = schedules[i]).state > SCHEDULED && schedule.name === name) {
+            return new Transition([[node]], root, name, +i);
+          }
+        }
+      }
+    
+      return null;
+    }
+    
+    var constant$7 = x => () => x;
+    
+    function BrushEvent(type, {
+      sourceEvent,
+      target,
+      selection,
+      mode,
+      dispatch
+    }) {
+      Object.defineProperties(this, {
+        type: {value: type, enumerable: true, configurable: true},
+        sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},
+        target: {value: target, enumerable: true, configurable: true},
+        selection: {value: selection, enumerable: true, configurable: true},
+        mode: {value: mode, enumerable: true, configurable: true},
+        _: {value: dispatch}
+      });
+    }
+    
+    function nopropagation$1(event) {
+      event.stopImmediatePropagation();
+    }
+    
+    function noevent$1(event) {
+      event.preventDefault();
+      event.stopImmediatePropagation();
+    }
+    
+    var MODE_DRAG = {name: "drag"},
+        MODE_SPACE = {name: "space"},
+        MODE_HANDLE = {name: "handle"},
+        MODE_CENTER = {name: "center"};
+    
+    const {abs: abs$3, max: max$2, min: min$1} = Math;
+    
+    function number1(e) {
+      return [+e[0], +e[1]];
+    }
+    
+    function number2(e) {
+      return [number1(e[0]), number1(e[1])];
+    }
+    
+    var X = {
+      name: "x",
+      handles: ["w", "e"].map(type),
+      input: function(x, e) { return x == null ? null : [[+x[0], e[0][1]], [+x[1], e[1][1]]]; },
+      output: function(xy) { return xy && [xy[0][0], xy[1][0]]; }
+    };
+    
+    var Y = {
+      name: "y",
+      handles: ["n", "s"].map(type),
+      input: function(y, e) { return y == null ? null : [[e[0][0], +y[0]], [e[1][0], +y[1]]]; },
+      output: function(xy) { return xy && [xy[0][1], xy[1][1]]; }
+    };
+    
+    var XY = {
+      name: "xy",
+      handles: ["n", "w", "e", "s", "nw", "ne", "sw", "se"].map(type),
+      input: function(xy) { return xy == null ? null : number2(xy); },
+      output: function(xy) { return xy; }
+    };
+    
+    var cursors = {
+      overlay: "crosshair",
+      selection: "move",
+      n: "ns-resize",
+      e: "ew-resize",
+      s: "ns-resize",
+      w: "ew-resize",
+      nw: "nwse-resize",
+      ne: "nesw-resize",
+      se: "nwse-resize",
+      sw: "nesw-resize"
+    };
+    
+    var flipX = {
+      e: "w",
+      w: "e",
+      nw: "ne",
+      ne: "nw",
+      se: "sw",
+      sw: "se"
+    };
+    
+    var flipY = {
+      n: "s",
+      s: "n",
+      nw: "sw",
+      ne: "se",
+      se: "ne",
+      sw: "nw"
+    };
+    
+    var signsX = {
+      overlay: +1,
+      selection: +1,
+      n: null,
+      e: +1,
+      s: null,
+      w: -1,
+      nw: -1,
+      ne: +1,
+      se: +1,
+      sw: -1
+    };
+    
+    var signsY = {
+      overlay: +1,
+      selection: +1,
+      n: -1,
+      e: null,
+      s: +1,
+      w: null,
+      nw: -1,
+      ne: -1,
+      se: +1,
+      sw: +1
+    };
+    
+    function type(t) {
+      return {type: t};
+    }
+    
+    // Ignore right-click, since that should open the context menu.
+    function defaultFilter$1(event) {
+      return !event.ctrlKey && !event.button;
+    }
+    
+    function defaultExtent$1() {
+      var svg = this.ownerSVGElement || this;
+      if (svg.hasAttribute("viewBox")) {
+        svg = svg.viewBox.baseVal;
+        return [[svg.x, svg.y], [svg.x + svg.width, svg.y + svg.height]];
+      }
+      return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]];
+    }
+    
+    function defaultTouchable$1() {
+      return navigator.maxTouchPoints || ("ontouchstart" in this);
+    }
+    
+    // Like d3.local, but with the name “__brush” rather than auto-generated.
+    function local(node) {
+      while (!node.__brush) if (!(node = node.parentNode)) return;
+      return node.__brush;
+    }
+    
+    function empty(extent) {
+      return extent[0][0] === extent[1][0]
+          || extent[0][1] === extent[1][1];
+    }
+    
+    function brushSelection(node) {
+      var state = node.__brush;
+      return state ? state.dim.output(state.selection) : null;
+    }
+    
+    function brushX() {
+      return brush$1(X);
+    }
+    
+    function brushY() {
+      return brush$1(Y);
+    }
+    
+    function brush() {
+      return brush$1(XY);
+    }
+    
+    function brush$1(dim) {
+      var extent = defaultExtent$1,
+          filter = defaultFilter$1,
+          touchable = defaultTouchable$1,
+          keys = true,
+          listeners = dispatch("start", "brush", "end"),
+          handleSize = 6,
+          touchending;
+    
+      function brush(group) {
+        var overlay = group
+            .property("__brush", initialize)
+          .selectAll(".overlay")
+          .data([type("overlay")]);
+    
+        overlay.enter().append("rect")
+            .attr("class", "overlay")
+            .attr("pointer-events", "all")
+            .attr("cursor", cursors.overlay)
+          .merge(overlay)
+            .each(function() {
+              var extent = local(this).extent;
+              select(this)
+                  .attr("x", extent[0][0])
+                  .attr("y", extent[0][1])
+                  .attr("width", extent[1][0] - extent[0][0])
+                  .attr("height", extent[1][1] - extent[0][1]);
+            });
+    
+        group.selectAll(".selection")
+          .data([type("selection")])
+          .enter().append("rect")
+            .attr("class", "selection")
+            .attr("cursor", cursors.selection)
+            .attr("fill", "#777")
+            .attr("fill-opacity", 0.3)
+            .attr("stroke", "#fff")
+            .attr("shape-rendering", "crispEdges");
+    
+        var handle = group.selectAll(".handle")
+          .data(dim.handles, function(d) { return d.type; });
+    
+        handle.exit().remove();
+    
+        handle.enter().append("rect")
+            .attr("class", function(d) { return "handle handle--" + d.type; })
+            .attr("cursor", function(d) { return cursors[d.type]; });
+    
+        group
+            .each(redraw)
+            .attr("fill", "none")
+            .attr("pointer-events", "all")
+            .on("mousedown.brush", started)
+          .filter(touchable)
+            .on("touchstart.brush", started)
+            .on("touchmove.brush", touchmoved)
+            .on("touchend.brush touchcancel.brush", touchended)
+            .style("touch-action", "none")
+            .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+      }
+    
+      brush.move = function(group, selection) {
+        if (group.tween) {
+          group
+              .on("start.brush", function(event) { emitter(this, arguments).beforestart().start(event); })
+              .on("interrupt.brush end.brush", function(event) { emitter(this, arguments).end(event); })
+              .tween("brush", function() {
+                var that = this,
+                    state = that.__brush,
+                    emit = emitter(that, arguments),
+                    selection0 = state.selection,
+                    selection1 = dim.input(typeof selection === "function" ? selection.apply(this, arguments) : selection, state.extent),
+                    i = interpolate$2(selection0, selection1);
+    
+                function tween(t) {
+                  state.selection = t === 1 && selection1 === null ? null : i(t);
+                  redraw.call(that);
+                  emit.brush();
+                }
+    
+                return selection0 !== null && selection1 !== null ? tween : tween(1);
+              });
+        } else {
+          group
+              .each(function() {
+                var that = this,
+                    args = arguments,
+                    state = that.__brush,
+                    selection1 = dim.input(typeof selection === "function" ? selection.apply(that, args) : selection, state.extent),
+                    emit = emitter(that, args).beforestart();
+    
+                interrupt(that);
+                state.selection = selection1 === null ? null : selection1;
+                redraw.call(that);
+                emit.start().brush().end();
+              });
+        }
+      };
+    
+      brush.clear = function(group) {
+        brush.move(group, null);
+      };
+    
+      function redraw() {
+        var group = select(this),
+            selection = local(this).selection;
+    
+        if (selection) {
+          group.selectAll(".selection")
+              .style("display", null)
+              .attr("x", selection[0][0])
+              .attr("y", selection[0][1])
+              .attr("width", selection[1][0] - selection[0][0])
+              .attr("height", selection[1][1] - selection[0][1]);
+    
+          group.selectAll(".handle")
+              .style("display", null)
+              .attr("x", function(d) { return d.type[d.type.length - 1] === "e" ? selection[1][0] - handleSize / 2 : selection[0][0] - handleSize / 2; })
+              .attr("y", function(d) { return d.type[0] === "s" ? selection[1][1] - handleSize / 2 : selection[0][1] - handleSize / 2; })
+              .attr("width", function(d) { return d.type === "n" || d.type === "s" ? selection[1][0] - selection[0][0] + handleSize : handleSize; })
+              .attr("height", function(d) { return d.type === "e" || d.type === "w" ? selection[1][1] - selection[0][1] + handleSize : handleSize; });
+        }
+    
+        else {
+          group.selectAll(".selection,.handle")
+              .style("display", "none")
+              .attr("x", null)
+              .attr("y", null)
+              .attr("width", null)
+              .attr("height", null);
+        }
+      }
+    
+      function emitter(that, args, clean) {
+        var emit = that.__brush.emitter;
+        return emit && (!clean || !emit.clean) ? emit : new Emitter(that, args, clean);
+      }
+    
+      function Emitter(that, args, clean) {
+        this.that = that;
+        this.args = args;
+        this.state = that.__brush;
+        this.active = 0;
+        this.clean = clean;
+      }
+    
+      Emitter.prototype = {
+        beforestart: function() {
+          if (++this.active === 1) this.state.emitter = this, this.starting = true;
+          return this;
+        },
+        start: function(event, mode) {
+          if (this.starting) this.starting = false, this.emit("start", event, mode);
+          else this.emit("brush", event);
+          return this;
+        },
+        brush: function(event, mode) {
+          this.emit("brush", event, mode);
+          return this;
+        },
+        end: function(event, mode) {
+          if (--this.active === 0) delete this.state.emitter, this.emit("end", event, mode);
+          return this;
+        },
+        emit: function(type, event, mode) {
+          var d = select(this.that).datum();
+          listeners.call(
+            type,
+            this.that,
+            new BrushEvent(type, {
+              sourceEvent: event,
+              target: brush,
+              selection: dim.output(this.state.selection),
+              mode,
+              dispatch: listeners
+            }),
+            d
+          );
+        }
+      };
+    
+      function started(event) {
+        if (touchending && !event.touches) return;
+        if (!filter.apply(this, arguments)) return;
+    
+        var that = this,
+            type = event.target.__data__.type,
+            mode = (keys && event.metaKey ? type = "overlay" : type) === "selection" ? MODE_DRAG : (keys && event.altKey ? MODE_CENTER : MODE_HANDLE),
+            signX = dim === Y ? null : signsX[type],
+            signY = dim === X ? null : signsY[type],
+            state = local(that),
+            extent = state.extent,
+            selection = state.selection,
+            W = extent[0][0], w0, w1,
+            N = extent[0][1], n0, n1,
+            E = extent[1][0], e0, e1,
+            S = extent[1][1], s0, s1,
+            dx = 0,
+            dy = 0,
+            moving,
+            shifting = signX && signY && keys && event.shiftKey,
+            lockX,
+            lockY,
+            points = Array.from(event.touches || [event], t => {
+              const i = t.identifier;
+              t = pointer(t, that);
+              t.point0 = t.slice();
+              t.identifier = i;
+              return t;
+            });
+    
+        if (type === "overlay") {
+          if (selection) moving = true;
+          const pts = [points[0], points[1] || points[0]];
+          state.selection = selection = [[
+              w0 = dim === Y ? W : min$1(pts[0][0], pts[1][0]),
+              n0 = dim === X ? N : min$1(pts[0][1], pts[1][1])
+            ], [
+              e0 = dim === Y ? E : max$2(pts[0][0], pts[1][0]),
+              s0 = dim === X ? S : max$2(pts[0][1], pts[1][1])
+            ]];
+          if (points.length > 1) move();
+        } else {
+          w0 = selection[0][0];
+          n0 = selection[0][1];
+          e0 = selection[1][0];
+          s0 = selection[1][1];
+        }
+    
+        w1 = w0;
+        n1 = n0;
+        e1 = e0;
+        s1 = s0;
+    
+        var group = select(that)
+            .attr("pointer-events", "none");
+    
+        var overlay = group.selectAll(".overlay")
+            .attr("cursor", cursors[type]);
+    
+        interrupt(that);
+        var emit = emitter(that, arguments, true).beforestart();
+    
+        if (event.touches) {
+          emit.moved = moved;
+          emit.ended = ended;
+        } else {
+          var view = select(event.view)
+              .on("mousemove.brush", moved, true)
+              .on("mouseup.brush", ended, true);
+          if (keys) view
+              .on("keydown.brush", keydowned, true)
+              .on("keyup.brush", keyupped, true);
+    
+          dragDisable(event.view);
+        }
+    
+        redraw.call(that);
+        emit.start(event, mode.name);
+    
+        function moved(event) {
+          for (const p of event.changedTouches || [event]) {
+            for (const d of points)
+              if (d.identifier === p.identifier) d.cur = pointer(p, that);
+          }
+          if (shifting && !lockX && !lockY && points.length === 1) {
+            const point = points[0];
+            if (abs$3(point.cur[0] - point[0]) > abs$3(point.cur[1] - point[1]))
+              lockY = true;
+            else
+              lockX = true;
+          }
+          for (const point of points)
+            if (point.cur) point[0] = point.cur[0], point[1] = point.cur[1];
+          moving = true;
+          noevent$1(event);
+          move(event);
+        }
+    
+        function move(event) {
+          const point = points[0], point0 = point.point0;
+          var t;
+    
+          dx = point[0] - point0[0];
+          dy = point[1] - point0[1];
+    
+          switch (mode) {
+            case MODE_SPACE:
+            case MODE_DRAG: {
+              if (signX) dx = max$2(W - w0, min$1(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx;
+              if (signY) dy = max$2(N - n0, min$1(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy;
+              break;
+            }
+            case MODE_HANDLE: {
+              if (points[1]) {
+                if (signX) w1 = max$2(W, min$1(E, points[0][0])), e1 = max$2(W, min$1(E, points[1][0])), signX = 1;
+                if (signY) n1 = max$2(N, min$1(S, points[0][1])), s1 = max$2(N, min$1(S, points[1][1])), signY = 1;
+              } else {
+                if (signX < 0) dx = max$2(W - w0, min$1(E - w0, dx)), w1 = w0 + dx, e1 = e0;
+                else if (signX > 0) dx = max$2(W - e0, min$1(E - e0, dx)), w1 = w0, e1 = e0 + dx;
+                if (signY < 0) dy = max$2(N - n0, min$1(S - n0, dy)), n1 = n0 + dy, s1 = s0;
+                else if (signY > 0) dy = max$2(N - s0, min$1(S - s0, dy)), n1 = n0, s1 = s0 + dy;
+              }
+              break;
+            }
+            case MODE_CENTER: {
+              if (signX) w1 = max$2(W, min$1(E, w0 - dx * signX)), e1 = max$2(W, min$1(E, e0 + dx * signX));
+              if (signY) n1 = max$2(N, min$1(S, n0 - dy * signY)), s1 = max$2(N, min$1(S, s0 + dy * signY));
+              break;
+            }
+          }
+    
+          if (e1 < w1) {
+            signX *= -1;
+            t = w0, w0 = e0, e0 = t;
+            t = w1, w1 = e1, e1 = t;
+            if (type in flipX) overlay.attr("cursor", cursors[type = flipX[type]]);
+          }
+    
+          if (s1 < n1) {
+            signY *= -1;
+            t = n0, n0 = s0, s0 = t;
+            t = n1, n1 = s1, s1 = t;
+            if (type in flipY) overlay.attr("cursor", cursors[type = flipY[type]]);
+          }
+    
+          if (state.selection) selection = state.selection; // May be set by brush.move!
+          if (lockX) w1 = selection[0][0], e1 = selection[1][0];
+          if (lockY) n1 = selection[0][1], s1 = selection[1][1];
+    
+          if (selection[0][0] !== w1
+              || selection[0][1] !== n1
+              || selection[1][0] !== e1
+              || selection[1][1] !== s1) {
+            state.selection = [[w1, n1], [e1, s1]];
+            redraw.call(that);
+            emit.brush(event, mode.name);
+          }
+        }
+    
+        function ended(event) {
+          nopropagation$1(event);
+          if (event.touches) {
+            if (event.touches.length) return;
+            if (touchending) clearTimeout(touchending);
+            touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
+          } else {
+            yesdrag(event.view, moving);
+            view.on("keydown.brush keyup.brush mousemove.brush mouseup.brush", null);
+          }
+          group.attr("pointer-events", "all");
+          overlay.attr("cursor", cursors.overlay);
+          if (state.selection) selection = state.selection; // May be set by brush.move (on start)!
+          if (empty(selection)) state.selection = null, redraw.call(that);
+          emit.end(event, mode.name);
+        }
+    
+        function keydowned(event) {
+          switch (event.keyCode) {
+            case 16: { // SHIFT
+              shifting = signX && signY;
+              break;
+            }
+            case 18: { // ALT
+              if (mode === MODE_HANDLE) {
+                if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
+                if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
+                mode = MODE_CENTER;
+                move();
+              }
+              break;
+            }
+            case 32: { // SPACE; takes priority over ALT
+              if (mode === MODE_HANDLE || mode === MODE_CENTER) {
+                if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx;
+                if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy;
+                mode = MODE_SPACE;
+                overlay.attr("cursor", cursors.selection);
+                move();
+              }
+              break;
+            }
+            default: return;
+          }
+          noevent$1(event);
+        }
+    
+        function keyupped(event) {
+          switch (event.keyCode) {
+            case 16: { // SHIFT
+              if (shifting) {
+                lockX = lockY = shifting = false;
+                move();
+              }
+              break;
+            }
+            case 18: { // ALT
+              if (mode === MODE_CENTER) {
+                if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
+                if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
+                mode = MODE_HANDLE;
+                move();
+              }
+              break;
+            }
+            case 32: { // SPACE
+              if (mode === MODE_SPACE) {
+                if (event.altKey) {
+                  if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
+                  if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
+                  mode = MODE_CENTER;
+                } else {
+                  if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
+                  if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
+                  mode = MODE_HANDLE;
+                }
+                overlay.attr("cursor", cursors[type]);
+                move();
+              }
+              break;
+            }
+            default: return;
+          }
+          noevent$1(event);
+        }
+      }
+    
+      function touchmoved(event) {
+        emitter(this, arguments).moved(event);
+      }
+    
+      function touchended(event) {
+        emitter(this, arguments).ended(event);
+      }
+    
+      function initialize() {
+        var state = this.__brush || {selection: null};
+        state.extent = number2(extent.apply(this, arguments));
+        state.dim = dim;
+        return state;
+      }
+    
+      brush.extent = function(_) {
+        return arguments.length ? (extent = typeof _ === "function" ? _ : constant$7(number2(_)), brush) : extent;
+      };
+    
+      brush.filter = function(_) {
+        return arguments.length ? (filter = typeof _ === "function" ? _ : constant$7(!!_), brush) : filter;
+      };
+    
+      brush.touchable = function(_) {
+        return arguments.length ? (touchable = typeof _ === "function" ? _ : constant$7(!!_), brush) : touchable;
+      };
+    
+      brush.handleSize = function(_) {
+        return arguments.length ? (handleSize = +_, brush) : handleSize;
+      };
+    
+      brush.keyModifiers = function(_) {
+        return arguments.length ? (keys = !!_, brush) : keys;
+      };
+    
+      brush.on = function() {
+        var value = listeners.on.apply(listeners, arguments);
+        return value === listeners ? brush : value;
+      };
+    
+      return brush;
+    }
+    
+    var abs$2 = Math.abs;
+    var cos$2 = Math.cos;
+    var sin$2 = Math.sin;
+    var pi$3 = Math.PI;
+    var halfPi$2 = pi$3 / 2;
+    var tau$4 = pi$3 * 2;
+    var max$1 = Math.max;
+    var epsilon$4 = 1e-12;
+    
+    function range$1(i, j) {
+      return Array.from({length: j - i}, (_, k) => i + k);
+    }
+    
+    function compareValue(compare) {
+      return function(a, b) {
+        return compare(
+          a.source.value + a.target.value,
+          b.source.value + b.target.value
+        );
+      };
+    }
+    
+    function chord() {
+      return chord$1(false, false);
+    }
+    
+    function chordTranspose() {
+      return chord$1(false, true);
+    }
+    
+    function chordDirected() {
+      return chord$1(true, false);
+    }
+    
+    function chord$1(directed, transpose) {
+      var padAngle = 0,
+          sortGroups = null,
+          sortSubgroups = null,
+          sortChords = null;
+    
+      function chord(matrix) {
+        var n = matrix.length,
+            groupSums = new Array(n),
+            groupIndex = range$1(0, n),
+            chords = new Array(n * n),
+            groups = new Array(n),
+            k = 0, dx;
+    
+        matrix = Float64Array.from({length: n * n}, transpose
+            ? (_, i) => matrix[i % n][i / n | 0]
+            : (_, i) => matrix[i / n | 0][i % n]);
+    
+        // Compute the scaling factor from value to angle in [0, 2pi].
+        for (let i = 0; i < n; ++i) {
+          let x = 0;
+          for (let j = 0; j < n; ++j) x += matrix[i * n + j] + directed * matrix[j * n + i];
+          k += groupSums[i] = x;
+        }
+        k = max$1(0, tau$4 - padAngle * n) / k;
+        dx = k ? padAngle : tau$4 / n;
+    
+        // Compute the angles for each group and constituent chord.
+        {
+          let x = 0;
+          if (sortGroups) groupIndex.sort((a, b) => sortGroups(groupSums[a], groupSums[b]));
+          for (const i of groupIndex) {
+            const x0 = x;
+            if (directed) {
+              const subgroupIndex = range$1(~n + 1, n).filter(j => j < 0 ? matrix[~j * n + i] : matrix[i * n + j]);
+              if (sortSubgroups) subgroupIndex.sort((a, b) => sortSubgroups(a < 0 ? -matrix[~a * n + i] : matrix[i * n + a], b < 0 ? -matrix[~b * n + i] : matrix[i * n + b]));
+              for (const j of subgroupIndex) {
+                if (j < 0) {
+                  const chord = chords[~j * n + i] || (chords[~j * n + i] = {source: null, target: null});
+                  chord.target = {index: i, startAngle: x, endAngle: x += matrix[~j * n + i] * k, value: matrix[~j * n + i]};
+                } else {
+                  const chord = chords[i * n + j] || (chords[i * n + j] = {source: null, target: null});
+                  chord.source = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
+                }
+              }
+              groups[i] = {index: i, startAngle: x0, endAngle: x, value: groupSums[i]};
+            } else {
+              const subgroupIndex = range$1(0, n).filter(j => matrix[i * n + j] || matrix[j * n + i]);
+              if (sortSubgroups) subgroupIndex.sort((a, b) => sortSubgroups(matrix[i * n + a], matrix[i * n + b]));
+              for (const j of subgroupIndex) {
+                let chord;
+                if (i < j) {
+                  chord = chords[i * n + j] || (chords[i * n + j] = {source: null, target: null});
+                  chord.source = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
+                } else {
+                  chord = chords[j * n + i] || (chords[j * n + i] = {source: null, target: null});
+                  chord.target = {index: i, startAngle: x, endAngle: x += matrix[i * n + j] * k, value: matrix[i * n + j]};
+                  if (i === j) chord.source = chord.target;
+                }
+                if (chord.source && chord.target && chord.source.value < chord.target.value) {
+                  const source = chord.source;
+                  chord.source = chord.target;
+                  chord.target = source;
+                }
+              }
+              groups[i] = {index: i, startAngle: x0, endAngle: x, value: groupSums[i]};
+            }
+            x += dx;
+          }
+        }
+    
+        // Remove empty chords.
+        chords = Object.values(chords);
+        chords.groups = groups;
+        return sortChords ? chords.sort(sortChords) : chords;
+      }
+    
+      chord.padAngle = function(_) {
+        return arguments.length ? (padAngle = max$1(0, _), chord) : padAngle;
+      };
+    
+      chord.sortGroups = function(_) {
+        return arguments.length ? (sortGroups = _, chord) : sortGroups;
+      };
+    
+      chord.sortSubgroups = function(_) {
+        return arguments.length ? (sortSubgroups = _, chord) : sortSubgroups;
+      };
+    
+      chord.sortChords = function(_) {
+        return arguments.length ? (_ == null ? sortChords = null : (sortChords = compareValue(_))._ = _, chord) : sortChords && sortChords._;
+      };
+    
+      return chord;
+    }
+    
+    const pi$2 = Math.PI,
+        tau$3 = 2 * pi$2,
+        epsilon$3 = 1e-6,
+        tauEpsilon = tau$3 - epsilon$3;
+    
+    function Path$1() {
+      this._x0 = this._y0 = // start of current subpath
+      this._x1 = this._y1 = null; // end of current subpath
+      this._ = "";
+    }
+    
+    function path() {
+      return new Path$1;
+    }
+    
+    Path$1.prototype = path.prototype = {
+      constructor: Path$1,
+      moveTo: function(x, y) {
+        this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
+      },
+      closePath: function() {
+        if (this._x1 !== null) {
+          this._x1 = this._x0, this._y1 = this._y0;
+          this._ += "Z";
+        }
+      },
+      lineTo: function(x, y) {
+        this._ += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
+      },
+      quadraticCurveTo: function(x1, y1, x, y) {
+        this._ += "Q" + (+x1) + "," + (+y1) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
+      },
+      bezierCurveTo: function(x1, y1, x2, y2, x, y) {
+        this._ += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
+      },
+      arcTo: function(x1, y1, x2, y2, r) {
+        x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r;
+        var x0 = this._x1,
+            y0 = this._y1,
+            x21 = x2 - x1,
+            y21 = y2 - y1,
+            x01 = x0 - x1,
+            y01 = y0 - y1,
+            l01_2 = x01 * x01 + y01 * y01;
+    
+        // Is the radius negative? Error.
+        if (r < 0) throw new Error("negative radius: " + r);
+    
+        // Is this path empty? Move to (x1,y1).
+        if (this._x1 === null) {
+          this._ += "M" + (this._x1 = x1) + "," + (this._y1 = y1);
+        }
+    
+        // Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
+        else if (!(l01_2 > epsilon$3));
+    
+        // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
+        // Equivalently, is (x1,y1) coincident with (x2,y2)?
+        // Or, is the radius zero? Line to (x1,y1).
+        else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon$3) || !r) {
+          this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1);
+        }
+    
+        // Otherwise, draw an arc!
+        else {
+          var x20 = x2 - x0,
+              y20 = y2 - y0,
+              l21_2 = x21 * x21 + y21 * y21,
+              l20_2 = x20 * x20 + y20 * y20,
+              l21 = Math.sqrt(l21_2),
+              l01 = Math.sqrt(l01_2),
+              l = r * Math.tan((pi$2 - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2),
+              t01 = l / l01,
+              t21 = l / l21;
+    
+          // If the start tangent is not coincident with (x0,y0), line to.
+          if (Math.abs(t01 - 1) > epsilon$3) {
+            this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01);
+          }
+    
+          this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21);
+        }
+      },
+      arc: function(x, y, r, a0, a1, ccw) {
+        x = +x, y = +y, r = +r, ccw = !!ccw;
+        var dx = r * Math.cos(a0),
+            dy = r * Math.sin(a0),
+            x0 = x + dx,
+            y0 = y + dy,
+            cw = 1 ^ ccw,
+            da = ccw ? a0 - a1 : a1 - a0;
+    
+        // Is the radius negative? Error.
+        if (r < 0) throw new Error("negative radius: " + r);
+    
+        // Is this path empty? Move to (x0,y0).
+        if (this._x1 === null) {
+          this._ += "M" + x0 + "," + y0;
+        }
+    
+        // Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0).
+        else if (Math.abs(this._x1 - x0) > epsilon$3 || Math.abs(this._y1 - y0) > epsilon$3) {
+          this._ += "L" + x0 + "," + y0;
+        }
+    
+        // Is this arc empty? We’re done.
+        if (!r) return;
+    
+        // Does the angle go the wrong way? Flip the direction.
+        if (da < 0) da = da % tau$3 + tau$3;
+    
+        // Is this a complete circle? Draw two arcs to complete the circle.
+        if (da > tauEpsilon) {
+          this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0);
+        }
+    
+        // Is this arc non-empty? Draw an arc!
+        else if (da > epsilon$3) {
+          this._ += "A" + r + "," + r + ",0," + (+(da >= pi$2)) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1));
+        }
+      },
+      rect: function(x, y, w, h) {
+        this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + (+w) + "v" + (+h) + "h" + (-w) + "Z";
+      },
+      toString: function() {
+        return this._;
+      }
+    };
+    
+    var slice$2 = Array.prototype.slice;
+    
+    function constant$6(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function defaultSource$1(d) {
+      return d.source;
+    }
+    
+    function defaultTarget(d) {
+      return d.target;
+    }
+    
+    function defaultRadius$1(d) {
+      return d.radius;
+    }
+    
+    function defaultStartAngle(d) {
+      return d.startAngle;
+    }
+    
+    function defaultEndAngle(d) {
+      return d.endAngle;
+    }
+    
+    function defaultPadAngle() {
+      return 0;
+    }
+    
+    function defaultArrowheadRadius() {
+      return 10;
+    }
+    
+    function ribbon(headRadius) {
+      var source = defaultSource$1,
+          target = defaultTarget,
+          sourceRadius = defaultRadius$1,
+          targetRadius = defaultRadius$1,
+          startAngle = defaultStartAngle,
+          endAngle = defaultEndAngle,
+          padAngle = defaultPadAngle,
+          context = null;
+    
+      function ribbon() {
+        var buffer,
+            s = source.apply(this, arguments),
+            t = target.apply(this, arguments),
+            ap = padAngle.apply(this, arguments) / 2,
+            argv = slice$2.call(arguments),
+            sr = +sourceRadius.apply(this, (argv[0] = s, argv)),
+            sa0 = startAngle.apply(this, argv) - halfPi$2,
+            sa1 = endAngle.apply(this, argv) - halfPi$2,
+            tr = +targetRadius.apply(this, (argv[0] = t, argv)),
+            ta0 = startAngle.apply(this, argv) - halfPi$2,
+            ta1 = endAngle.apply(this, argv) - halfPi$2;
+    
+        if (!context) context = buffer = path();
+    
+        if (ap > epsilon$4) {
+          if (abs$2(sa1 - sa0) > ap * 2 + epsilon$4) sa1 > sa0 ? (sa0 += ap, sa1 -= ap) : (sa0 -= ap, sa1 += ap);
+          else sa0 = sa1 = (sa0 + sa1) / 2;
+          if (abs$2(ta1 - ta0) > ap * 2 + epsilon$4) ta1 > ta0 ? (ta0 += ap, ta1 -= ap) : (ta0 -= ap, ta1 += ap);
+          else ta0 = ta1 = (ta0 + ta1) / 2;
+        }
+    
+        context.moveTo(sr * cos$2(sa0), sr * sin$2(sa0));
+        context.arc(0, 0, sr, sa0, sa1);
+        if (sa0 !== ta0 || sa1 !== ta1) {
+          if (headRadius) {
+            var hr = +headRadius.apply(this, arguments), tr2 = tr - hr, ta2 = (ta0 + ta1) / 2;
+            context.quadraticCurveTo(0, 0, tr2 * cos$2(ta0), tr2 * sin$2(ta0));
+            context.lineTo(tr * cos$2(ta2), tr * sin$2(ta2));
+            context.lineTo(tr2 * cos$2(ta1), tr2 * sin$2(ta1));
+          } else {
+            context.quadraticCurveTo(0, 0, tr * cos$2(ta0), tr * sin$2(ta0));
+            context.arc(0, 0, tr, ta0, ta1);
+          }
+        }
+        context.quadraticCurveTo(0, 0, sr * cos$2(sa0), sr * sin$2(sa0));
+        context.closePath();
+    
+        if (buffer) return context = null, buffer + "" || null;
+      }
+    
+      if (headRadius) ribbon.headRadius = function(_) {
+        return arguments.length ? (headRadius = typeof _ === "function" ? _ : constant$6(+_), ribbon) : headRadius;
+      };
+    
+      ribbon.radius = function(_) {
+        return arguments.length ? (sourceRadius = targetRadius = typeof _ === "function" ? _ : constant$6(+_), ribbon) : sourceRadius;
+      };
+    
+      ribbon.sourceRadius = function(_) {
+        return arguments.length ? (sourceRadius = typeof _ === "function" ? _ : constant$6(+_), ribbon) : sourceRadius;
+      };
+    
+      ribbon.targetRadius = function(_) {
+        return arguments.length ? (targetRadius = typeof _ === "function" ? _ : constant$6(+_), ribbon) : targetRadius;
+      };
+    
+      ribbon.startAngle = function(_) {
+        return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$6(+_), ribbon) : startAngle;
+      };
+    
+      ribbon.endAngle = function(_) {
+        return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$6(+_), ribbon) : endAngle;
+      };
+    
+      ribbon.padAngle = function(_) {
+        return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$6(+_), ribbon) : padAngle;
+      };
+    
+      ribbon.source = function(_) {
+        return arguments.length ? (source = _, ribbon) : source;
+      };
+    
+      ribbon.target = function(_) {
+        return arguments.length ? (target = _, ribbon) : target;
+      };
+    
+      ribbon.context = function(_) {
+        return arguments.length ? ((context = _ == null ? null : _), ribbon) : context;
+      };
+    
+      return ribbon;
+    }
+    
+    function ribbon$1() {
+      return ribbon();
+    }
+    
+    function ribbonArrow() {
+      return ribbon(defaultArrowheadRadius);
+    }
+    
+    var array$2 = Array.prototype;
+    
+    var slice$1 = array$2.slice;
+    
+    function ascending$1(a, b) {
+      return a - b;
+    }
+    
+    function area$3(ring) {
+      var i = 0, n = ring.length, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1];
+      while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1];
+      return area;
+    }
+    
+    var constant$5 = x => () => x;
+    
+    function contains$2(ring, hole) {
+      var i = -1, n = hole.length, c;
+      while (++i < n) if (c = ringContains(ring, hole[i])) return c;
+      return 0;
+    }
+    
+    function ringContains(ring, point) {
+      var x = point[0], y = point[1], contains = -1;
+      for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) {
+        var pi = ring[i], xi = pi[0], yi = pi[1], pj = ring[j], xj = pj[0], yj = pj[1];
+        if (segmentContains(pi, pj, point)) return 0;
+        if (((yi > y) !== (yj > y)) && ((x < (xj - xi) * (y - yi) / (yj - yi) + xi))) contains = -contains;
+      }
+      return contains;
+    }
+    
+    function segmentContains(a, b, c) {
+      var i; return collinear$1(a, b, c) && within(a[i = +(a[0] === b[0])], c[i], b[i]);
+    }
+    
+    function collinear$1(a, b, c) {
+      return (b[0] - a[0]) * (c[1] - a[1]) === (c[0] - a[0]) * (b[1] - a[1]);
+    }
+    
+    function within(p, q, r) {
+      return p <= q && q <= r || r <= q && q <= p;
+    }
+    
+    function noop$2() {}
+    
+    var cases = [
+      [],
+      [[[1.0, 1.5], [0.5, 1.0]]],
+      [[[1.5, 1.0], [1.0, 1.5]]],
+      [[[1.5, 1.0], [0.5, 1.0]]],
+      [[[1.0, 0.5], [1.5, 1.0]]],
+      [[[1.0, 1.5], [0.5, 1.0]], [[1.0, 0.5], [1.5, 1.0]]],
+      [[[1.0, 0.5], [1.0, 1.5]]],
+      [[[1.0, 0.5], [0.5, 1.0]]],
+      [[[0.5, 1.0], [1.0, 0.5]]],
+      [[[1.0, 1.5], [1.0, 0.5]]],
+      [[[0.5, 1.0], [1.0, 0.5]], [[1.5, 1.0], [1.0, 1.5]]],
+      [[[1.5, 1.0], [1.0, 0.5]]],
+      [[[0.5, 1.0], [1.5, 1.0]]],
+      [[[1.0, 1.5], [1.5, 1.0]]],
+      [[[0.5, 1.0], [1.0, 1.5]]],
+      []
+    ];
+    
+    function contours() {
+      var dx = 1,
+          dy = 1,
+          threshold = thresholdSturges,
+          smooth = smoothLinear;
+    
+      function contours(values) {
+        var tz = threshold(values);
+    
+        // Convert number of thresholds into uniform thresholds.
+        if (!Array.isArray(tz)) {
+          var domain = extent$1(values), start = domain[0], stop = domain[1];
+          tz = tickStep(start, stop, tz);
+          tz = sequence(Math.floor(start / tz) * tz, Math.floor(stop / tz) * tz, tz);
+        } else {
+          tz = tz.slice().sort(ascending$1);
+        }
+    
+        return tz.map(function(value) {
+          return contour(values, value);
+        });
+      }
+    
+      // Accumulate, smooth contour rings, assign holes to exterior rings.
+      // Based on https://github.com/mbostock/shapefile/blob/v0.6.2/shp/polygon.js
+      function contour(values, value) {
+        var polygons = [],
+            holes = [];
+    
+        isorings(values, value, function(ring) {
+          smooth(ring, values, value);
+          if (area$3(ring) > 0) polygons.push([ring]);
+          else holes.push(ring);
+        });
+    
+        holes.forEach(function(hole) {
+          for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
+            if (contains$2((polygon = polygons[i])[0], hole) !== -1) {
+              polygon.push(hole);
+              return;
+            }
+          }
+        });
+    
+        return {
+          type: "MultiPolygon",
+          value: value,
+          coordinates: polygons
+        };
+      }
+    
+      // Marching squares with isolines stitched into rings.
+      // Based on https://github.com/topojson/topojson-client/blob/v3.0.0/src/stitch.js
+      function isorings(values, value, callback) {
+        var fragmentByStart = new Array,
+            fragmentByEnd = new Array,
+            x, y, t0, t1, t2, t3;
+    
+        // Special case for the first row (y = -1, t2 = t3 = 0).
+        x = y = -1;
+        t1 = values[0] >= value;
+        cases[t1 << 1].forEach(stitch);
+        while (++x < dx - 1) {
+          t0 = t1, t1 = values[x + 1] >= value;
+          cases[t0 | t1 << 1].forEach(stitch);
+        }
+        cases[t1 << 0].forEach(stitch);
+    
+        // General case for the intermediate rows.
+        while (++y < dy - 1) {
+          x = -1;
+          t1 = values[y * dx + dx] >= value;
+          t2 = values[y * dx] >= value;
+          cases[t1 << 1 | t2 << 2].forEach(stitch);
+          while (++x < dx - 1) {
+            t0 = t1, t1 = values[y * dx + dx + x + 1] >= value;
+            t3 = t2, t2 = values[y * dx + x + 1] >= value;
+            cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
+          }
+          cases[t1 | t2 << 3].forEach(stitch);
+        }
+    
+        // Special case for the last row (y = dy - 1, t0 = t1 = 0).
+        x = -1;
+        t2 = values[y * dx] >= value;
+        cases[t2 << 2].forEach(stitch);
+        while (++x < dx - 1) {
+          t3 = t2, t2 = values[y * dx + x + 1] >= value;
+          cases[t2 << 2 | t3 << 3].forEach(stitch);
+        }
+        cases[t2 << 3].forEach(stitch);
+    
+        function stitch(line) {
+          var start = [line[0][0] + x, line[0][1] + y],
+              end = [line[1][0] + x, line[1][1] + y],
+              startIndex = index(start),
+              endIndex = index(end),
+              f, g;
+          if (f = fragmentByEnd[startIndex]) {
+            if (g = fragmentByStart[endIndex]) {
+              delete fragmentByEnd[f.end];
+              delete fragmentByStart[g.start];
+              if (f === g) {
+                f.ring.push(end);
+                callback(f.ring);
+              } else {
+                fragmentByStart[f.start] = fragmentByEnd[g.end] = {start: f.start, end: g.end, ring: f.ring.concat(g.ring)};
+              }
+            } else {
+              delete fragmentByEnd[f.end];
+              f.ring.push(end);
+              fragmentByEnd[f.end = endIndex] = f;
+            }
+          } else if (f = fragmentByStart[endIndex]) {
+            if (g = fragmentByEnd[startIndex]) {
+              delete fragmentByStart[f.start];
+              delete fragmentByEnd[g.end];
+              if (f === g) {
+                f.ring.push(end);
+                callback(f.ring);
+              } else {
+                fragmentByStart[g.start] = fragmentByEnd[f.end] = {start: g.start, end: f.end, ring: g.ring.concat(f.ring)};
+              }
+            } else {
+              delete fragmentByStart[f.start];
+              f.ring.unshift(start);
+              fragmentByStart[f.start = startIndex] = f;
+            }
+          } else {
+            fragmentByStart[startIndex] = fragmentByEnd[endIndex] = {start: startIndex, end: endIndex, ring: [start, end]};
+          }
+        }
+      }
+    
+      function index(point) {
+        return point[0] * 2 + point[1] * (dx + 1) * 4;
+      }
+    
+      function smoothLinear(ring, values, value) {
+        ring.forEach(function(point) {
+          var x = point[0],
+              y = point[1],
+              xt = x | 0,
+              yt = y | 0,
+              v0,
+              v1 = values[yt * dx + xt];
+          if (x > 0 && x < dx && xt === x) {
+            v0 = values[yt * dx + xt - 1];
+            point[0] = x + (value - v0) / (v1 - v0) - 0.5;
+          }
+          if (y > 0 && y < dy && yt === y) {
+            v0 = values[(yt - 1) * dx + xt];
+            point[1] = y + (value - v0) / (v1 - v0) - 0.5;
+          }
+        });
+      }
+    
+      contours.contour = contour;
+    
+      contours.size = function(_) {
+        if (!arguments.length) return [dx, dy];
+        var _0 = Math.floor(_[0]), _1 = Math.floor(_[1]);
+        if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
+        return dx = _0, dy = _1, contours;
+      };
+    
+      contours.thresholds = function(_) {
+        return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant$5(slice$1.call(_)) : constant$5(_), contours) : threshold;
+      };
+    
+      contours.smooth = function(_) {
+        return arguments.length ? (smooth = _ ? smoothLinear : noop$2, contours) : smooth === smoothLinear;
+      };
+    
+      return contours;
+    }
+    
+    // TODO Optimize edge cases.
+    // TODO Optimize index calculation.
+    // TODO Optimize arguments.
+    function blurX(source, target, r) {
+      var n = source.width,
+          m = source.height,
+          w = (r << 1) + 1;
+      for (var j = 0; j < m; ++j) {
+        for (var i = 0, sr = 0; i < n + r; ++i) {
+          if (i < n) {
+            sr += source.data[i + j * n];
+          }
+          if (i >= r) {
+            if (i >= w) {
+              sr -= source.data[i - w + j * n];
+            }
+            target.data[i - r + j * n] = sr / Math.min(i + 1, n - 1 + w - i, w);
+          }
+        }
+      }
+    }
+    
+    // TODO Optimize edge cases.
+    // TODO Optimize index calculation.
+    // TODO Optimize arguments.
+    function blurY(source, target, r) {
+      var n = source.width,
+          m = source.height,
+          w = (r << 1) + 1;
+      for (var i = 0; i < n; ++i) {
+        for (var j = 0, sr = 0; j < m + r; ++j) {
+          if (j < m) {
+            sr += source.data[i + j * n];
+          }
+          if (j >= r) {
+            if (j >= w) {
+              sr -= source.data[i + (j - w) * n];
+            }
+            target.data[i + (j - r) * n] = sr / Math.min(j + 1, m - 1 + w - j, w);
+          }
+        }
+      }
+    }
+    
+    function defaultX$1(d) {
+      return d[0];
+    }
+    
+    function defaultY$1(d) {
+      return d[1];
+    }
+    
+    function defaultWeight() {
+      return 1;
+    }
+    
+    function density() {
+      var x = defaultX$1,
+          y = defaultY$1,
+          weight = defaultWeight,
+          dx = 960,
+          dy = 500,
+          r = 20, // blur radius
+          k = 2, // log2(grid cell size)
+          o = r * 3, // grid offset, to pad for blur
+          n = (dx + o * 2) >> k, // grid width
+          m = (dy + o * 2) >> k, // grid height
+          threshold = constant$5(20);
+    
+      function density(data) {
+        var values0 = new Float32Array(n * m),
+            values1 = new Float32Array(n * m);
+    
+        data.forEach(function(d, i, data) {
+          var xi = (+x(d, i, data) + o) >> k,
+              yi = (+y(d, i, data) + o) >> k,
+              wi = +weight(d, i, data);
+          if (xi >= 0 && xi < n && yi >= 0 && yi < m) {
+            values0[xi + yi * n] += wi;
+          }
+        });
+    
+        // TODO Optimize.
+        blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+        blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+        blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+        blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+        blurX({width: n, height: m, data: values0}, {width: n, height: m, data: values1}, r >> k);
+        blurY({width: n, height: m, data: values1}, {width: n, height: m, data: values0}, r >> k);
+    
+        var tz = threshold(values0);
+    
+        // Convert number of thresholds into uniform thresholds.
+        if (!Array.isArray(tz)) {
+          var stop = max$3(values0);
+          tz = tickStep(0, stop, tz);
+          tz = sequence(0, Math.floor(stop / tz) * tz, tz);
+          tz.shift();
+        }
+    
+        return contours()
+            .thresholds(tz)
+            .size([n, m])
+          (values0)
+            .map(transform);
+      }
+    
+      function transform(geometry) {
+        geometry.value *= Math.pow(2, -2 * k); // Density in points per square pixel.
+        geometry.coordinates.forEach(transformPolygon);
+        return geometry;
+      }
+    
+      function transformPolygon(coordinates) {
+        coordinates.forEach(transformRing);
+      }
+    
+      function transformRing(coordinates) {
+        coordinates.forEach(transformPoint);
+      }
+    
+      // TODO Optimize.
+      function transformPoint(coordinates) {
+        coordinates[0] = coordinates[0] * Math.pow(2, k) - o;
+        coordinates[1] = coordinates[1] * Math.pow(2, k) - o;
+      }
+    
+      function resize() {
+        o = r * 3;
+        n = (dx + o * 2) >> k;
+        m = (dy + o * 2) >> k;
+        return density;
+      }
+    
+      density.x = function(_) {
+        return arguments.length ? (x = typeof _ === "function" ? _ : constant$5(+_), density) : x;
+      };
+    
+      density.y = function(_) {
+        return arguments.length ? (y = typeof _ === "function" ? _ : constant$5(+_), density) : y;
+      };
+    
+      density.weight = function(_) {
+        return arguments.length ? (weight = typeof _ === "function" ? _ : constant$5(+_), density) : weight;
+      };
+    
+      density.size = function(_) {
+        if (!arguments.length) return [dx, dy];
+        var _0 = +_[0], _1 = +_[1];
+        if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
+        return dx = _0, dy = _1, resize();
+      };
+    
+      density.cellSize = function(_) {
+        if (!arguments.length) return 1 << k;
+        if (!((_ = +_) >= 1)) throw new Error("invalid cell size");
+        return k = Math.floor(Math.log(_) / Math.LN2), resize();
+      };
+    
+      density.thresholds = function(_) {
+        return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant$5(slice$1.call(_)) : constant$5(_), density) : threshold;
+      };
+    
+      density.bandwidth = function(_) {
+        if (!arguments.length) return Math.sqrt(r * (r + 1));
+        if (!((_ = +_) >= 0)) throw new Error("invalid bandwidth");
+        return r = Math.round((Math.sqrt(4 * _ * _ + 1) - 1) / 2), resize();
+      };
+    
+      return density;
+    }
+    
+    const EPSILON = Math.pow(2, -52);
+    const EDGE_STACK = new Uint32Array(512);
+    
+    class Delaunator {
+    
+        static from(points, getX = defaultGetX, getY = defaultGetY) {
+            const n = points.length;
+            const coords = new Float64Array(n * 2);
+    
+            for (let i = 0; i < n; i++) {
+                const p = points[i];
+                coords[2 * i] = getX(p);
+                coords[2 * i + 1] = getY(p);
+            }
+    
+            return new Delaunator(coords);
+        }
+    
+        constructor(coords) {
+            const n = coords.length >> 1;
+            if (n > 0 && typeof coords[0] !== 'number') throw new Error('Expected coords to contain numbers.');
+    
+            this.coords = coords;
+    
+            // arrays that will store the triangulation graph
+            const maxTriangles = Math.max(2 * n - 5, 0);
+            this._triangles = new Uint32Array(maxTriangles * 3);
+            this._halfedges = new Int32Array(maxTriangles * 3);
+    
+            // temporary arrays for tracking the edges of the advancing convex hull
+            this._hashSize = Math.ceil(Math.sqrt(n));
+            this._hullPrev = new Uint32Array(n); // edge to prev edge
+            this._hullNext = new Uint32Array(n); // edge to next edge
+            this._hullTri = new Uint32Array(n); // edge to adjacent triangle
+            this._hullHash = new Int32Array(this._hashSize).fill(-1); // angular edge hash
+    
+            // temporary arrays for sorting points
+            this._ids = new Uint32Array(n);
+            this._dists = new Float64Array(n);
+    
+            this.update();
+        }
+    
+        update() {
+            const {coords, _hullPrev: hullPrev, _hullNext: hullNext, _hullTri: hullTri, _hullHash: hullHash} =  this;
+            const n = coords.length >> 1;
+    
+            // populate an array of point indices; calculate input data bbox
+            let minX = Infinity;
+            let minY = Infinity;
+            let maxX = -Infinity;
+            let maxY = -Infinity;
+    
+            for (let i = 0; i < n; i++) {
+                const x = coords[2 * i];
+                const y = coords[2 * i + 1];
+                if (x < minX) minX = x;
+                if (y < minY) minY = y;
+                if (x > maxX) maxX = x;
+                if (y > maxY) maxY = y;
+                this._ids[i] = i;
+            }
+            const cx = (minX + maxX) / 2;
+            const cy = (minY + maxY) / 2;
+    
+            let minDist = Infinity;
+            let i0, i1, i2;
+    
+            // pick a seed point close to the center
+            for (let i = 0; i < n; i++) {
+                const d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]);
+                if (d < minDist) {
+                    i0 = i;
+                    minDist = d;
+                }
+            }
+            const i0x = coords[2 * i0];
+            const i0y = coords[2 * i0 + 1];
+    
+            minDist = Infinity;
+    
+            // find the point closest to the seed
+            for (let i = 0; i < n; i++) {
+                if (i === i0) continue;
+                const d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]);
+                if (d < minDist && d > 0) {
+                    i1 = i;
+                    minDist = d;
+                }
+            }
+            let i1x = coords[2 * i1];
+            let i1y = coords[2 * i1 + 1];
+    
+            let minRadius = Infinity;
+    
+            // find the third point which forms the smallest circumcircle with the first two
+            for (let i = 0; i < n; i++) {
+                if (i === i0 || i === i1) continue;
+                const r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]);
+                if (r < minRadius) {
+                    i2 = i;
+                    minRadius = r;
+                }
+            }
+            let i2x = coords[2 * i2];
+            let i2y = coords[2 * i2 + 1];
+    
+            if (minRadius === Infinity) {
+                // order collinear points by dx (or dy if all x are identical)
+                // and return the list as a hull
+                for (let i = 0; i < n; i++) {
+                    this._dists[i] = (coords[2 * i] - coords[0]) || (coords[2 * i + 1] - coords[1]);
+                }
+                quicksort(this._ids, this._dists, 0, n - 1);
+                const hull = new Uint32Array(n);
+                let j = 0;
+                for (let i = 0, d0 = -Infinity; i < n; i++) {
+                    const id = this._ids[i];
+                    if (this._dists[id] > d0) {
+                        hull[j++] = id;
+                        d0 = this._dists[id];
+                    }
+                }
+                this.hull = hull.subarray(0, j);
+                this.triangles = new Uint32Array(0);
+                this.halfedges = new Uint32Array(0);
+                return;
+            }
+    
+            // swap the order of the seed points for counter-clockwise orientation
+            if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) {
+                const i = i1;
+                const x = i1x;
+                const y = i1y;
+                i1 = i2;
+                i1x = i2x;
+                i1y = i2y;
+                i2 = i;
+                i2x = x;
+                i2y = y;
+            }
+    
+            const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y);
+            this._cx = center.x;
+            this._cy = center.y;
+    
+            for (let i = 0; i < n; i++) {
+                this._dists[i] = dist(coords[2 * i], coords[2 * i + 1], center.x, center.y);
+            }
+    
+            // sort the points by distance from the seed triangle circumcenter
+            quicksort(this._ids, this._dists, 0, n - 1);
+    
+            // set up the seed triangle as the starting hull
+            this._hullStart = i0;
+            let hullSize = 3;
+    
+            hullNext[i0] = hullPrev[i2] = i1;
+            hullNext[i1] = hullPrev[i0] = i2;
+            hullNext[i2] = hullPrev[i1] = i0;
+    
+            hullTri[i0] = 0;
+            hullTri[i1] = 1;
+            hullTri[i2] = 2;
+    
+            hullHash.fill(-1);
+            hullHash[this._hashKey(i0x, i0y)] = i0;
+            hullHash[this._hashKey(i1x, i1y)] = i1;
+            hullHash[this._hashKey(i2x, i2y)] = i2;
+    
+            this.trianglesLen = 0;
+            this._addTriangle(i0, i1, i2, -1, -1, -1);
+    
+            for (let k = 0, xp, yp; k < this._ids.length; k++) {
+                const i = this._ids[k];
+                const x = coords[2 * i];
+                const y = coords[2 * i + 1];
+    
+                // skip near-duplicate points
+                if (k > 0 && Math.abs(x - xp) <= EPSILON && Math.abs(y - yp) <= EPSILON) continue;
+                xp = x;
+                yp = y;
+    
+                // skip seed triangle points
+                if (i === i0 || i === i1 || i === i2) continue;
+    
+                // find a visible edge on the convex hull using edge hash
+                let start = 0;
+                for (let j = 0, key = this._hashKey(x, y); j < this._hashSize; j++) {
+                    start = hullHash[(key + j) % this._hashSize];
+                    if (start !== -1 && start !== hullNext[start]) break;
+                }
+    
+                start = hullPrev[start];
+                let e = start, q;
+                while (q = hullNext[e], !orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1])) {
+                    e = q;
+                    if (e === start) {
+                        e = -1;
+                        break;
+                    }
+                }
+                if (e === -1) continue; // likely a near-duplicate point; skip it
+    
+                // add the first triangle from the point
+                let t = this._addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]);
+    
+                // recursively flip triangles from the point until they satisfy the Delaunay condition
+                hullTri[i] = this._legalize(t + 2);
+                hullTri[e] = t; // keep track of boundary triangles on the hull
+                hullSize++;
+    
+                // walk forward through the hull, adding more triangles and flipping recursively
+                let n = hullNext[e];
+                while (q = hullNext[n], orient(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1])) {
+                    t = this._addTriangle(n, i, q, hullTri[i], -1, hullTri[n]);
+                    hullTri[i] = this._legalize(t + 2);
+                    hullNext[n] = n; // mark as removed
+                    hullSize--;
+                    n = q;
+                }
+    
+                // walk backward from the other side, adding more triangles and flipping
+                if (e === start) {
+                    while (q = hullPrev[e], orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1])) {
+                        t = this._addTriangle(q, i, e, -1, hullTri[e], hullTri[q]);
+                        this._legalize(t + 2);
+                        hullTri[q] = t;
+                        hullNext[e] = e; // mark as removed
+                        hullSize--;
+                        e = q;
+                    }
+                }
+    
+                // update the hull indices
+                this._hullStart = hullPrev[i] = e;
+                hullNext[e] = hullPrev[n] = i;
+                hullNext[i] = n;
+    
+                // save the two new edges in the hash table
+                hullHash[this._hashKey(x, y)] = i;
+                hullHash[this._hashKey(coords[2 * e], coords[2 * e + 1])] = e;
+            }
+    
+            this.hull = new Uint32Array(hullSize);
+            for (let i = 0, e = this._hullStart; i < hullSize; i++) {
+                this.hull[i] = e;
+                e = hullNext[e];
+            }
+    
+            // trim typed triangle mesh arrays
+            this.triangles = this._triangles.subarray(0, this.trianglesLen);
+            this.halfedges = this._halfedges.subarray(0, this.trianglesLen);
+        }
+    
+        _hashKey(x, y) {
+            return Math.floor(pseudoAngle(x - this._cx, y - this._cy) * this._hashSize) % this._hashSize;
+        }
+    
+        _legalize(a) {
+            const {_triangles: triangles, _halfedges: halfedges, coords} = this;
+    
+            let i = 0;
+            let ar = 0;
+    
+            // recursion eliminated with a fixed-size stack
+            while (true) {
+                const b = halfedges[a];
+    
+                /* if the pair of triangles doesn't satisfy the Delaunay condition
+                 * (p1 is inside the circumcircle of [p0, pl, pr]), flip them,
+                 * then do the same check/flip recursively for the new pair of triangles
+                 *
+                 *           pl                    pl
+                 *          /||\                  /  \
+                 *       al/ || \bl            al/    \a
+                 *        /  ||  \              /      \
+                 *       /  a||b  \    flip    /___ar___\
+                 *     p0\   ||   /p1   =>   p0\---bl---/p1
+                 *        \  ||  /              \      /
+                 *       ar\ || /br             b\    /br
+                 *          \||/                  \  /
+                 *           pr                    pr
+                 */
+                const a0 = a - a % 3;
+                ar = a0 + (a + 2) % 3;
+    
+                if (b === -1) { // convex hull edge
+                    if (i === 0) break;
+                    a = EDGE_STACK[--i];
+                    continue;
+                }
+    
+                const b0 = b - b % 3;
+                const al = a0 + (a + 1) % 3;
+                const bl = b0 + (b + 2) % 3;
+    
+                const p0 = triangles[ar];
+                const pr = triangles[a];
+                const pl = triangles[al];
+                const p1 = triangles[bl];
+    
+                const illegal = inCircle(
+                    coords[2 * p0], coords[2 * p0 + 1],
+                    coords[2 * pr], coords[2 * pr + 1],
+                    coords[2 * pl], coords[2 * pl + 1],
+                    coords[2 * p1], coords[2 * p1 + 1]);
+    
+                if (illegal) {
+                    triangles[a] = p1;
+                    triangles[b] = p0;
+    
+                    const hbl = halfedges[bl];
+    
+                    // edge swapped on the other side of the hull (rare); fix the halfedge reference
+                    if (hbl === -1) {
+                        let e = this._hullStart;
+                        do {
+                            if (this._hullTri[e] === bl) {
+                                this._hullTri[e] = a;
+                                break;
+                            }
+                            e = this._hullPrev[e];
+                        } while (e !== this._hullStart);
+                    }
+                    this._link(a, hbl);
+                    this._link(b, halfedges[ar]);
+                    this._link(ar, bl);
+    
+                    const br = b0 + (b + 1) % 3;
+    
+                    // don't worry about hitting the cap: it can only happen on extremely degenerate input
+                    if (i < EDGE_STACK.length) {
+                        EDGE_STACK[i++] = br;
+                    }
+                } else {
+                    if (i === 0) break;
+                    a = EDGE_STACK[--i];
+                }
+            }
+    
+            return ar;
+        }
+    
+        _link(a, b) {
+            this._halfedges[a] = b;
+            if (b !== -1) this._halfedges[b] = a;
+        }
+    
+        // add a new triangle given vertex indices and adjacent half-edge ids
+        _addTriangle(i0, i1, i2, a, b, c) {
+            const t = this.trianglesLen;
+    
+            this._triangles[t] = i0;
+            this._triangles[t + 1] = i1;
+            this._triangles[t + 2] = i2;
+    
+            this._link(t, a);
+            this._link(t + 1, b);
+            this._link(t + 2, c);
+    
+            this.trianglesLen += 3;
+    
+            return t;
+        }
+    }
+    
+    // monotonically increases with real angle, but doesn't need expensive trigonometry
+    function pseudoAngle(dx, dy) {
+        const p = dx / (Math.abs(dx) + Math.abs(dy));
+        return (dy > 0 ? 3 - p : 1 + p) / 4; // [0..1]
+    }
+    
+    function dist(ax, ay, bx, by) {
+        const dx = ax - bx;
+        const dy = ay - by;
+        return dx * dx + dy * dy;
+    }
+    
+    // return 2d orientation sign if we're confident in it through J. Shewchuk's error bound check
+    function orientIfSure(px, py, rx, ry, qx, qy) {
+        const l = (ry - py) * (qx - px);
+        const r = (rx - px) * (qy - py);
+        return Math.abs(l - r) >= 3.3306690738754716e-16 * Math.abs(l + r) ? l - r : 0;
+    }
+    
+    // a more robust orientation test that's stable in a given triangle (to fix robustness issues)
+    function orient(rx, ry, qx, qy, px, py) {
+        const sign = orientIfSure(px, py, rx, ry, qx, qy) ||
+        orientIfSure(rx, ry, qx, qy, px, py) ||
+        orientIfSure(qx, qy, px, py, rx, ry);
+        return sign < 0;
+    }
+    
+    function inCircle(ax, ay, bx, by, cx, cy, px, py) {
+        const dx = ax - px;
+        const dy = ay - py;
+        const ex = bx - px;
+        const ey = by - py;
+        const fx = cx - px;
+        const fy = cy - py;
+    
+        const ap = dx * dx + dy * dy;
+        const bp = ex * ex + ey * ey;
+        const cp = fx * fx + fy * fy;
+    
+        return dx * (ey * cp - bp * fy) -
+               dy * (ex * cp - bp * fx) +
+               ap * (ex * fy - ey * fx) < 0;
+    }
+    
+    function circumradius(ax, ay, bx, by, cx, cy) {
+        const dx = bx - ax;
+        const dy = by - ay;
+        const ex = cx - ax;
+        const ey = cy - ay;
+    
+        const bl = dx * dx + dy * dy;
+        const cl = ex * ex + ey * ey;
+        const d = 0.5 / (dx * ey - dy * ex);
+    
+        const x = (ey * bl - dy * cl) * d;
+        const y = (dx * cl - ex * bl) * d;
+    
+        return x * x + y * y;
+    }
+    
+    function circumcenter(ax, ay, bx, by, cx, cy) {
+        const dx = bx - ax;
+        const dy = by - ay;
+        const ex = cx - ax;
+        const ey = cy - ay;
+    
+        const bl = dx * dx + dy * dy;
+        const cl = ex * ex + ey * ey;
+        const d = 0.5 / (dx * ey - dy * ex);
+    
+        const x = ax + (ey * bl - dy * cl) * d;
+        const y = ay + (dx * cl - ex * bl) * d;
+    
+        return {x, y};
+    }
+    
+    function quicksort(ids, dists, left, right) {
+        if (right - left <= 20) {
+            for (let i = left + 1; i <= right; i++) {
+                const temp = ids[i];
+                const tempDist = dists[temp];
+                let j = i - 1;
+                while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--];
+                ids[j + 1] = temp;
+            }
+        } else {
+            const median = (left + right) >> 1;
+            let i = left + 1;
+            let j = right;
+            swap(ids, median, i);
+            if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right);
+            if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right);
+            if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i);
+    
+            const temp = ids[i];
+            const tempDist = dists[temp];
+            while (true) {
+                do i++; while (dists[ids[i]] < tempDist);
+                do j--; while (dists[ids[j]] > tempDist);
+                if (j < i) break;
+                swap(ids, i, j);
+            }
+            ids[left + 1] = ids[j];
+            ids[j] = temp;
+    
+            if (right - i + 1 >= j - left) {
+                quicksort(ids, dists, i, right);
+                quicksort(ids, dists, left, j - 1);
+            } else {
+                quicksort(ids, dists, left, j - 1);
+                quicksort(ids, dists, i, right);
+            }
+        }
+    }
+    
+    function swap(arr, i, j) {
+        const tmp = arr[i];
+        arr[i] = arr[j];
+        arr[j] = tmp;
+    }
+    
+    function defaultGetX(p) {
+        return p[0];
+    }
+    function defaultGetY(p) {
+        return p[1];
+    }
+    
+    const epsilon$2 = 1e-6;
+    
+    class Path {
+      constructor() {
+        this._x0 = this._y0 = // start of current subpath
+        this._x1 = this._y1 = null; // end of current subpath
+        this._ = "";
+      }
+      moveTo(x, y) {
+        this._ += `M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`;
+      }
+      closePath() {
+        if (this._x1 !== null) {
+          this._x1 = this._x0, this._y1 = this._y0;
+          this._ += "Z";
+        }
+      }
+      lineTo(x, y) {
+        this._ += `L${this._x1 = +x},${this._y1 = +y}`;
+      }
+      arc(x, y, r) {
+        x = +x, y = +y, r = +r;
+        const x0 = x + r;
+        const y0 = y;
+        if (r < 0) throw new Error("negative radius");
+        if (this._x1 === null) this._ += `M${x0},${y0}`;
+        else if (Math.abs(this._x1 - x0) > epsilon$2 || Math.abs(this._y1 - y0) > epsilon$2) this._ += "L" + x0 + "," + y0;
+        if (!r) return;
+        this._ += `A${r},${r},0,1,1,${x - r},${y}A${r},${r},0,1,1,${this._x1 = x0},${this._y1 = y0}`;
+      }
+      rect(x, y, w, h) {
+        this._ += `M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}h${+w}v${+h}h${-w}Z`;
+      }
+      value() {
+        return this._ || null;
+      }
+    }
+    
+    class Polygon {
+      constructor() {
+        this._ = [];
+      }
+      moveTo(x, y) {
+        this._.push([x, y]);
+      }
+      closePath() {
+        this._.push(this._[0].slice());
+      }
+      lineTo(x, y) {
+        this._.push([x, y]);
+      }
+      value() {
+        return this._.length ? this._ : null;
+      }
+    }
+    
+    class Voronoi {
+      constructor(delaunay, [xmin, ymin, xmax, ymax] = [0, 0, 960, 500]) {
+        if (!((xmax = +xmax) >= (xmin = +xmin)) || !((ymax = +ymax) >= (ymin = +ymin))) throw new Error("invalid bounds");
+        this.delaunay = delaunay;
+        this._circumcenters = new Float64Array(delaunay.points.length * 2);
+        this.vectors = new Float64Array(delaunay.points.length * 2);
+        this.xmax = xmax, this.xmin = xmin;
+        this.ymax = ymax, this.ymin = ymin;
+        this._init();
+      }
+      update() {
+        this.delaunay.update();
+        this._init();
+        return this;
+      }
+      _init() {
+        const {delaunay: {points, hull, triangles}, vectors} = this;
+    
+        // Compute circumcenters.
+        const circumcenters = this.circumcenters = this._circumcenters.subarray(0, triangles.length / 3 * 2);
+        for (let i = 0, j = 0, n = triangles.length, x, y; i < n; i += 3, j += 2) {
+          const t1 = triangles[i] * 2;
+          const t2 = triangles[i + 1] * 2;
+          const t3 = triangles[i + 2] * 2;
+          const x1 = points[t1];
+          const y1 = points[t1 + 1];
+          const x2 = points[t2];
+          const y2 = points[t2 + 1];
+          const x3 = points[t3];
+          const y3 = points[t3 + 1];
+    
+          const dx = x2 - x1;
+          const dy = y2 - y1;
+          const ex = x3 - x1;
+          const ey = y3 - y1;
+          const bl = dx * dx + dy * dy;
+          const cl = ex * ex + ey * ey;
+          const ab = (dx * ey - dy * ex) * 2;
+    
+          if (!ab) {
+            // degenerate case (collinear diagram)
+            x = (x1 + x3) / 2 - 1e8 * ey;
+            y = (y1 + y3) / 2 + 1e8 * ex;
+          }
+          else if (Math.abs(ab) < 1e-8) {
+            // almost equal points (degenerate triangle)
+            x = (x1 + x3) / 2;
+            y = (y1 + y3) / 2;
+          } else {
+            const d = 1 / ab;
+            x = x1 + (ey * bl - dy * cl) * d;
+            y = y1 + (dx * cl - ex * bl) * d;
+          }
+          circumcenters[j] = x;
+          circumcenters[j + 1] = y;
+        }
+    
+        // Compute exterior cell rays.
+        let h = hull[hull.length - 1];
+        let p0, p1 = h * 4;
+        let x0, x1 = points[2 * h];
+        let y0, y1 = points[2 * h + 1];
+        vectors.fill(0);
+        for (let i = 0; i < hull.length; ++i) {
+          h = hull[i];
+          p0 = p1, x0 = x1, y0 = y1;
+          p1 = h * 4, x1 = points[2 * h], y1 = points[2 * h + 1];
+          vectors[p0 + 2] = vectors[p1] = y0 - y1;
+          vectors[p0 + 3] = vectors[p1 + 1] = x1 - x0;
+        }
+      }
+      render(context) {
+        const buffer = context == null ? context = new Path : undefined;
+        const {delaunay: {halfedges, inedges, hull}, circumcenters, vectors} = this;
+        if (hull.length <= 1) return null;
+        for (let i = 0, n = halfedges.length; i < n; ++i) {
+          const j = halfedges[i];
+          if (j < i) continue;
+          const ti = Math.floor(i / 3) * 2;
+          const tj = Math.floor(j / 3) * 2;
+          const xi = circumcenters[ti];
+          const yi = circumcenters[ti + 1];
+          const xj = circumcenters[tj];
+          const yj = circumcenters[tj + 1];
+          this._renderSegment(xi, yi, xj, yj, context);
+        }
+        let h0, h1 = hull[hull.length - 1];
+        for (let i = 0; i < hull.length; ++i) {
+          h0 = h1, h1 = hull[i];
+          const t = Math.floor(inedges[h1] / 3) * 2;
+          const x = circumcenters[t];
+          const y = circumcenters[t + 1];
+          const v = h0 * 4;
+          const p = this._project(x, y, vectors[v + 2], vectors[v + 3]);
+          if (p) this._renderSegment(x, y, p[0], p[1], context);
+        }
+        return buffer && buffer.value();
+      }
+      renderBounds(context) {
+        const buffer = context == null ? context = new Path : undefined;
+        context.rect(this.xmin, this.ymin, this.xmax - this.xmin, this.ymax - this.ymin);
+        return buffer && buffer.value();
+      }
+      renderCell(i, context) {
+        const buffer = context == null ? context = new Path : undefined;
+        const points = this._clip(i);
+        if (points === null || !points.length) return;
+        context.moveTo(points[0], points[1]);
+        let n = points.length;
+        while (points[0] === points[n-2] && points[1] === points[n-1] && n > 1) n -= 2;
+        for (let i = 2; i < n; i += 2) {
+          if (points[i] !== points[i-2] || points[i+1] !== points[i-1])
+            context.lineTo(points[i], points[i + 1]);
+        }
+        context.closePath();
+        return buffer && buffer.value();
+      }
+      *cellPolygons() {
+        const {delaunay: {points}} = this;
+        for (let i = 0, n = points.length / 2; i < n; ++i) {
+          const cell = this.cellPolygon(i);
+          if (cell) cell.index = i, yield cell;
+        }
+      }
+      cellPolygon(i) {
+        const polygon = new Polygon;
+        this.renderCell(i, polygon);
+        return polygon.value();
+      }
+      _renderSegment(x0, y0, x1, y1, context) {
+        let S;
+        const c0 = this._regioncode(x0, y0);
+        const c1 = this._regioncode(x1, y1);
+        if (c0 === 0 && c1 === 0) {
+          context.moveTo(x0, y0);
+          context.lineTo(x1, y1);
+        } else if (S = this._clipSegment(x0, y0, x1, y1, c0, c1)) {
+          context.moveTo(S[0], S[1]);
+          context.lineTo(S[2], S[3]);
+        }
+      }
+      contains(i, x, y) {
+        if ((x = +x, x !== x) || (y = +y, y !== y)) return false;
+        return this.delaunay._step(i, x, y) === i;
+      }
+      *neighbors(i) {
+        const ci = this._clip(i);
+        if (ci) for (const j of this.delaunay.neighbors(i)) {
+          const cj = this._clip(j);
+          // find the common edge
+          if (cj) loop: for (let ai = 0, li = ci.length; ai < li; ai += 2) {
+            for (let aj = 0, lj = cj.length; aj < lj; aj += 2) {
+              if (ci[ai] == cj[aj]
+              && ci[ai + 1] == cj[aj + 1]
+              && ci[(ai + 2) % li] == cj[(aj + lj - 2) % lj]
+              && ci[(ai + 3) % li] == cj[(aj + lj - 1) % lj]
+              ) {
+                yield j;
+                break loop;
+              }
+            }
+          }
+        }
+      }
+      _cell(i) {
+        const {circumcenters, delaunay: {inedges, halfedges, triangles}} = this;
+        const e0 = inedges[i];
+        if (e0 === -1) return null; // coincident point
+        const points = [];
+        let e = e0;
+        do {
+          const t = Math.floor(e / 3);
+          points.push(circumcenters[t * 2], circumcenters[t * 2 + 1]);
+          e = e % 3 === 2 ? e - 2 : e + 1;
+          if (triangles[e] !== i) break; // bad triangulation
+          e = halfedges[e];
+        } while (e !== e0 && e !== -1);
+        return points;
+      }
+      _clip(i) {
+        // degenerate case (1 valid point: return the box)
+        if (i === 0 && this.delaunay.hull.length === 1) {
+          return [this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax, this.xmin, this.ymin];
+        }
+        const points = this._cell(i);
+        if (points === null) return null;
+        const {vectors: V} = this;
+        const v = i * 4;
+        return V[v] || V[v + 1]
+            ? this._clipInfinite(i, points, V[v], V[v + 1], V[v + 2], V[v + 3])
+            : this._clipFinite(i, points);
+      }
+      _clipFinite(i, points) {
+        const n = points.length;
+        let P = null;
+        let x0, y0, x1 = points[n - 2], y1 = points[n - 1];
+        let c0, c1 = this._regioncode(x1, y1);
+        let e0, e1;
+        for (let j = 0; j < n; j += 2) {
+          x0 = x1, y0 = y1, x1 = points[j], y1 = points[j + 1];
+          c0 = c1, c1 = this._regioncode(x1, y1);
+          if (c0 === 0 && c1 === 0) {
+            e0 = e1, e1 = 0;
+            if (P) P.push(x1, y1);
+            else P = [x1, y1];
+          } else {
+            let S, sx0, sy0, sx1, sy1;
+            if (c0 === 0) {
+              if ((S = this._clipSegment(x0, y0, x1, y1, c0, c1)) === null) continue;
+              [sx0, sy0, sx1, sy1] = S;
+            } else {
+              if ((S = this._clipSegment(x1, y1, x0, y0, c1, c0)) === null) continue;
+              [sx1, sy1, sx0, sy0] = S;
+              e0 = e1, e1 = this._edgecode(sx0, sy0);
+              if (e0 && e1) this._edge(i, e0, e1, P, P.length);
+              if (P) P.push(sx0, sy0);
+              else P = [sx0, sy0];
+            }
+            e0 = e1, e1 = this._edgecode(sx1, sy1);
+            if (e0 && e1) this._edge(i, e0, e1, P, P.length);
+            if (P) P.push(sx1, sy1);
+            else P = [sx1, sy1];
+          }
+        }
+        if (P) {
+          e0 = e1, e1 = this._edgecode(P[0], P[1]);
+          if (e0 && e1) this._edge(i, e0, e1, P, P.length);
+        } else if (this.contains(i, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) {
+          return [this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax, this.xmin, this.ymin];
+        }
+        return P;
+      }
+      _clipSegment(x0, y0, x1, y1, c0, c1) {
+        while (true) {
+          if (c0 === 0 && c1 === 0) return [x0, y0, x1, y1];
+          if (c0 & c1) return null;
+          let x, y, c = c0 || c1;
+          if (c & 0b1000) x = x0 + (x1 - x0) * (this.ymax - y0) / (y1 - y0), y = this.ymax;
+          else if (c & 0b0100) x = x0 + (x1 - x0) * (this.ymin - y0) / (y1 - y0), y = this.ymin;
+          else if (c & 0b0010) y = y0 + (y1 - y0) * (this.xmax - x0) / (x1 - x0), x = this.xmax;
+          else y = y0 + (y1 - y0) * (this.xmin - x0) / (x1 - x0), x = this.xmin;
+          if (c0) x0 = x, y0 = y, c0 = this._regioncode(x0, y0);
+          else x1 = x, y1 = y, c1 = this._regioncode(x1, y1);
+        }
+      }
+      _clipInfinite(i, points, vx0, vy0, vxn, vyn) {
+        let P = Array.from(points), p;
+        if (p = this._project(P[0], P[1], vx0, vy0)) P.unshift(p[0], p[1]);
+        if (p = this._project(P[P.length - 2], P[P.length - 1], vxn, vyn)) P.push(p[0], p[1]);
+        if (P = this._clipFinite(i, P)) {
+          for (let j = 0, n = P.length, c0, c1 = this._edgecode(P[n - 2], P[n - 1]); j < n; j += 2) {
+            c0 = c1, c1 = this._edgecode(P[j], P[j + 1]);
+            if (c0 && c1) j = this._edge(i, c0, c1, P, j), n = P.length;
+          }
+        } else if (this.contains(i, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) {
+          P = [this.xmin, this.ymin, this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax];
+        }
+        return P;
+      }
+      _edge(i, e0, e1, P, j) {
+        while (e0 !== e1) {
+          let x, y;
+          switch (e0) {
+            case 0b0101: e0 = 0b0100; continue; // top-left
+            case 0b0100: e0 = 0b0110, x = this.xmax, y = this.ymin; break; // top
+            case 0b0110: e0 = 0b0010; continue; // top-right
+            case 0b0010: e0 = 0b1010, x = this.xmax, y = this.ymax; break; // right
+            case 0b1010: e0 = 0b1000; continue; // bottom-right
+            case 0b1000: e0 = 0b1001, x = this.xmin, y = this.ymax; break; // bottom
+            case 0b1001: e0 = 0b0001; continue; // bottom-left
+            case 0b0001: e0 = 0b0101, x = this.xmin, y = this.ymin; break; // left
+          }
+          if ((P[j] !== x || P[j + 1] !== y) && this.contains(i, x, y)) {
+            P.splice(j, 0, x, y), j += 2;
+          }
+        }
+        if (P.length > 4) {
+          for (let i = 0; i < P.length; i+= 2) {
+            const j = (i + 2) % P.length, k = (i + 4) % P.length;
+            if (P[i] === P[j] && P[j] === P[k]
+            || P[i + 1] === P[j + 1] && P[j + 1] === P[k + 1])
+              P.splice(j, 2), i -= 2;
+          }
+        }
+        return j;
+      }
+      _project(x0, y0, vx, vy) {
+        let t = Infinity, c, x, y;
+        if (vy < 0) { // top
+          if (y0 <= this.ymin) return null;
+          if ((c = (this.ymin - y0) / vy) < t) y = this.ymin, x = x0 + (t = c) * vx;
+        } else if (vy > 0) { // bottom
+          if (y0 >= this.ymax) return null;
+          if ((c = (this.ymax - y0) / vy) < t) y = this.ymax, x = x0 + (t = c) * vx;
+        }
+        if (vx > 0) { // right
+          if (x0 >= this.xmax) return null;
+          if ((c = (this.xmax - x0) / vx) < t) x = this.xmax, y = y0 + (t = c) * vy;
+        } else if (vx < 0) { // left
+          if (x0 <= this.xmin) return null;
+          if ((c = (this.xmin - x0) / vx) < t) x = this.xmin, y = y0 + (t = c) * vy;
+        }
+        return [x, y];
+      }
+      _edgecode(x, y) {
+        return (x === this.xmin ? 0b0001
+            : x === this.xmax ? 0b0010 : 0b0000)
+            | (y === this.ymin ? 0b0100
+            : y === this.ymax ? 0b1000 : 0b0000);
+      }
+      _regioncode(x, y) {
+        return (x < this.xmin ? 0b0001
+            : x > this.xmax ? 0b0010 : 0b0000)
+            | (y < this.ymin ? 0b0100
+            : y > this.ymax ? 0b1000 : 0b0000);
+      }
+    }
+    
+    const tau$2 = 2 * Math.PI, pow$2 = Math.pow;
+    
+    function pointX(p) {
+      return p[0];
+    }
+    
+    function pointY(p) {
+      return p[1];
+    }
+    
+    // A triangulation is collinear if all its triangles have a non-null area
+    function collinear(d) {
+      const {triangles, coords} = d;
+      for (let i = 0; i < triangles.length; i += 3) {
+        const a = 2 * triangles[i],
+              b = 2 * triangles[i + 1],
+              c = 2 * triangles[i + 2],
+              cross = (coords[c] - coords[a]) * (coords[b + 1] - coords[a + 1])
+                    - (coords[b] - coords[a]) * (coords[c + 1] - coords[a + 1]);
+        if (cross > 1e-10) return false;
+      }
+      return true;
+    }
+    
+    function jitter(x, y, r) {
+      return [x + Math.sin(x + y) * r, y + Math.cos(x - y) * r];
+    }
+    
+    class Delaunay {
+      static from(points, fx = pointX, fy = pointY, that) {
+        return new Delaunay("length" in points
+            ? flatArray(points, fx, fy, that)
+            : Float64Array.from(flatIterable(points, fx, fy, that)));
+      }
+      constructor(points) {
+        this._delaunator = new Delaunator(points);
+        this.inedges = new Int32Array(points.length / 2);
+        this._hullIndex = new Int32Array(points.length / 2);
+        this.points = this._delaunator.coords;
+        this._init();
+      }
+      update() {
+        this._delaunator.update();
+        this._init();
+        return this;
+      }
+      _init() {
+        const d = this._delaunator, points = this.points;
+    
+        // check for collinear
+        if (d.hull && d.hull.length > 2 && collinear(d)) {
+          this.collinear = Int32Array.from({length: points.length/2}, (_,i) => i)
+            .sort((i, j) => points[2 * i] - points[2 * j] || points[2 * i + 1] - points[2 * j + 1]); // for exact neighbors
+          const e = this.collinear[0], f = this.collinear[this.collinear.length - 1],
+            bounds = [ points[2 * e], points[2 * e + 1], points[2 * f], points[2 * f + 1] ],
+            r = 1e-8 * Math.hypot(bounds[3] - bounds[1], bounds[2] - bounds[0]);
+          for (let i = 0, n = points.length / 2; i < n; ++i) {
+            const p = jitter(points[2 * i], points[2 * i + 1], r);
+            points[2 * i] = p[0];
+            points[2 * i + 1] = p[1];
+          }
+          this._delaunator = new Delaunator(points);
+        } else {
+          delete this.collinear;
+        }
+    
+        const halfedges = this.halfedges = this._delaunator.halfedges;
+        const hull = this.hull = this._delaunator.hull;
+        const triangles = this.triangles = this._delaunator.triangles;
+        const inedges = this.inedges.fill(-1);
+        const hullIndex = this._hullIndex.fill(-1);
+    
+        // Compute an index from each point to an (arbitrary) incoming halfedge
+        // Used to give the first neighbor of each point; for this reason,
+        // on the hull we give priority to exterior halfedges
+        for (let e = 0, n = halfedges.length; e < n; ++e) {
+          const p = triangles[e % 3 === 2 ? e - 2 : e + 1];
+          if (halfedges[e] === -1 || inedges[p] === -1) inedges[p] = e;
+        }
+        for (let i = 0, n = hull.length; i < n; ++i) {
+          hullIndex[hull[i]] = i;
+        }
+    
+        // degenerate case: 1 or 2 (distinct) points
+        if (hull.length <= 2 && hull.length > 0) {
+          this.triangles = new Int32Array(3).fill(-1);
+          this.halfedges = new Int32Array(3).fill(-1);
+          this.triangles[0] = hull[0];
+          this.triangles[1] = hull[1];
+          this.triangles[2] = hull[1];
+          inedges[hull[0]] = 1;
+          if (hull.length === 2) inedges[hull[1]] = 0;
+        }
+      }
+      voronoi(bounds) {
+        return new Voronoi(this, bounds);
+      }
+      *neighbors(i) {
+        const {inedges, hull, _hullIndex, halfedges, triangles, collinear} = this;
+    
+        // degenerate case with several collinear points
+        if (collinear) {
+          const l = collinear.indexOf(i);
+          if (l > 0) yield collinear[l - 1];
+          if (l < collinear.length - 1) yield collinear[l + 1];
+          return;
+        }
+    
+        const e0 = inedges[i];
+        if (e0 === -1) return; // coincident point
+        let e = e0, p0 = -1;
+        do {
+          yield p0 = triangles[e];
+          e = e % 3 === 2 ? e - 2 : e + 1;
+          if (triangles[e] !== i) return; // bad triangulation
+          e = halfedges[e];
+          if (e === -1) {
+            const p = hull[(_hullIndex[i] + 1) % hull.length];
+            if (p !== p0) yield p;
+            return;
+          }
+        } while (e !== e0);
+      }
+      find(x, y, i = 0) {
+        if ((x = +x, x !== x) || (y = +y, y !== y)) return -1;
+        const i0 = i;
+        let c;
+        while ((c = this._step(i, x, y)) >= 0 && c !== i && c !== i0) i = c;
+        return c;
+      }
+      _step(i, x, y) {
+        const {inedges, hull, _hullIndex, halfedges, triangles, points} = this;
+        if (inedges[i] === -1 || !points.length) return (i + 1) % (points.length >> 1);
+        let c = i;
+        let dc = pow$2(x - points[i * 2], 2) + pow$2(y - points[i * 2 + 1], 2);
+        const e0 = inedges[i];
+        let e = e0;
+        do {
+          let t = triangles[e];
+          const dt = pow$2(x - points[t * 2], 2) + pow$2(y - points[t * 2 + 1], 2);
+          if (dt < dc) dc = dt, c = t;
+          e = e % 3 === 2 ? e - 2 : e + 1;
+          if (triangles[e] !== i) break; // bad triangulation
+          e = halfedges[e];
+          if (e === -1) {
+            e = hull[(_hullIndex[i] + 1) % hull.length];
+            if (e !== t) {
+              if (pow$2(x - points[e * 2], 2) + pow$2(y - points[e * 2 + 1], 2) < dc) return e;
+            }
+            break;
+          }
+        } while (e !== e0);
+        return c;
+      }
+      render(context) {
+        const buffer = context == null ? context = new Path : undefined;
+        const {points, halfedges, triangles} = this;
+        for (let i = 0, n = halfedges.length; i < n; ++i) {
+          const j = halfedges[i];
+          if (j < i) continue;
+          const ti = triangles[i] * 2;
+          const tj = triangles[j] * 2;
+          context.moveTo(points[ti], points[ti + 1]);
+          context.lineTo(points[tj], points[tj + 1]);
+        }
+        this.renderHull(context);
+        return buffer && buffer.value();
+      }
+      renderPoints(context, r = 2) {
+        const buffer = context == null ? context = new Path : undefined;
+        const {points} = this;
+        for (let i = 0, n = points.length; i < n; i += 2) {
+          const x = points[i], y = points[i + 1];
+          context.moveTo(x + r, y);
+          context.arc(x, y, r, 0, tau$2);
+        }
+        return buffer && buffer.value();
+      }
+      renderHull(context) {
+        const buffer = context == null ? context = new Path : undefined;
+        const {hull, points} = this;
+        const h = hull[0] * 2, n = hull.length;
+        context.moveTo(points[h], points[h + 1]);
+        for (let i = 1; i < n; ++i) {
+          const h = 2 * hull[i];
+          context.lineTo(points[h], points[h + 1]);
+        }
+        context.closePath();
+        return buffer && buffer.value();
+      }
+      hullPolygon() {
+        const polygon = new Polygon;
+        this.renderHull(polygon);
+        return polygon.value();
+      }
+      renderTriangle(i, context) {
+        const buffer = context == null ? context = new Path : undefined;
+        const {points, triangles} = this;
+        const t0 = triangles[i *= 3] * 2;
+        const t1 = triangles[i + 1] * 2;
+        const t2 = triangles[i + 2] * 2;
+        context.moveTo(points[t0], points[t0 + 1]);
+        context.lineTo(points[t1], points[t1 + 1]);
+        context.lineTo(points[t2], points[t2 + 1]);
+        context.closePath();
+        return buffer && buffer.value();
+      }
+      *trianglePolygons() {
+        const {triangles} = this;
+        for (let i = 0, n = triangles.length / 3; i < n; ++i) {
+          yield this.trianglePolygon(i);
+        }
+      }
+      trianglePolygon(i) {
+        const polygon = new Polygon;
+        this.renderTriangle(i, polygon);
+        return polygon.value();
+      }
+    }
+    
+    function flatArray(points, fx, fy, that) {
+      const n = points.length;
+      const array = new Float64Array(n * 2);
+      for (let i = 0; i < n; ++i) {
+        const p = points[i];
+        array[i * 2] = fx.call(that, p, i, points);
+        array[i * 2 + 1] = fy.call(that, p, i, points);
+      }
+      return array;
+    }
+    
+    function* flatIterable(points, fx, fy, that) {
+      let i = 0;
+      for (const p of points) {
+        yield fx.call(that, p, i, points);
+        yield fy.call(that, p, i, points);
+        ++i;
+      }
+    }
+    
+    var EOL = {},
+        EOF = {},
+        QUOTE = 34,
+        NEWLINE = 10,
+        RETURN = 13;
+    
+    function objectConverter(columns) {
+      return new Function("d", "return {" + columns.map(function(name, i) {
+        return JSON.stringify(name) + ": d[" + i + "] || \"\"";
+      }).join(",") + "}");
+    }
+    
+    function customConverter(columns, f) {
+      var object = objectConverter(columns);
+      return function(row, i) {
+        return f(object(row), i, columns);
+      };
+    }
+    
+    // Compute unique columns in order of discovery.
+    function inferColumns(rows) {
+      var columnSet = Object.create(null),
+          columns = [];
+    
+      rows.forEach(function(row) {
+        for (var column in row) {
+          if (!(column in columnSet)) {
+            columns.push(columnSet[column] = column);
+          }
+        }
+      });
+    
+      return columns;
+    }
+    
+    function pad$1(value, width) {
+      var s = value + "", length = s.length;
+      return length < width ? new Array(width - length + 1).join(0) + s : s;
+    }
+    
+    function formatYear$1(year) {
+      return year < 0 ? "-" + pad$1(-year, 6)
+        : year > 9999 ? "+" + pad$1(year, 6)
+        : pad$1(year, 4);
+    }
+    
+    function formatDate(date) {
+      var hours = date.getUTCHours(),
+          minutes = date.getUTCMinutes(),
+          seconds = date.getUTCSeconds(),
+          milliseconds = date.getUTCMilliseconds();
+      return isNaN(date) ? "Invalid Date"
+          : formatYear$1(date.getUTCFullYear()) + "-" + pad$1(date.getUTCMonth() + 1, 2) + "-" + pad$1(date.getUTCDate(), 2)
+          + (milliseconds ? "T" + pad$1(hours, 2) + ":" + pad$1(minutes, 2) + ":" + pad$1(seconds, 2) + "." + pad$1(milliseconds, 3) + "Z"
+          : seconds ? "T" + pad$1(hours, 2) + ":" + pad$1(minutes, 2) + ":" + pad$1(seconds, 2) + "Z"
+          : minutes || hours ? "T" + pad$1(hours, 2) + ":" + pad$1(minutes, 2) + "Z"
+          : "");
+    }
+    
+    function dsvFormat(delimiter) {
+      var reFormat = new RegExp("[\"" + delimiter + "\n\r]"),
+          DELIMITER = delimiter.charCodeAt(0);
+    
+      function parse(text, f) {
+        var convert, columns, rows = parseRows(text, function(row, i) {
+          if (convert) return convert(row, i - 1);
+          columns = row, convert = f ? customConverter(row, f) : objectConverter(row);
+        });
+        rows.columns = columns || [];
+        return rows;
+      }
+    
+      function parseRows(text, f) {
+        var rows = [], // output rows
+            N = text.length,
+            I = 0, // current character index
+            n = 0, // current line number
+            t, // current token
+            eof = N <= 0, // current token followed by EOF?
+            eol = false; // current token followed by EOL?
+    
+        // Strip the trailing newline.
+        if (text.charCodeAt(N - 1) === NEWLINE) --N;
+        if (text.charCodeAt(N - 1) === RETURN) --N;
+    
+        function token() {
+          if (eof) return EOF;
+          if (eol) return eol = false, EOL;
+    
+          // Unescape quotes.
+          var i, j = I, c;
+          if (text.charCodeAt(j) === QUOTE) {
+            while (I++ < N && text.charCodeAt(I) !== QUOTE || text.charCodeAt(++I) === QUOTE);
+            if ((i = I) >= N) eof = true;
+            else if ((c = text.charCodeAt(I++)) === NEWLINE) eol = true;
+            else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }
+            return text.slice(j + 1, i - 1).replace(/""/g, "\"");
+          }
+    
+          // Find next delimiter or newline.
+          while (I < N) {
+            if ((c = text.charCodeAt(i = I++)) === NEWLINE) eol = true;
+            else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }
+            else if (c !== DELIMITER) continue;
+            return text.slice(j, i);
+          }
+    
+          // Return last token before EOF.
+          return eof = true, text.slice(j, N);
+        }
+    
+        while ((t = token()) !== EOF) {
+          var row = [];
+          while (t !== EOL && t !== EOF) row.push(t), t = token();
+          if (f && (row = f(row, n++)) == null) continue;
+          rows.push(row);
+        }
+    
+        return rows;
+      }
+    
+      function preformatBody(rows, columns) {
+        return rows.map(function(row) {
+          return columns.map(function(column) {
+            return formatValue(row[column]);
+          }).join(delimiter);
+        });
+      }
+    
+      function format(rows, columns) {
+        if (columns == null) columns = inferColumns(rows);
+        return [columns.map(formatValue).join(delimiter)].concat(preformatBody(rows, columns)).join("\n");
+      }
+    
+      function formatBody(rows, columns) {
+        if (columns == null) columns = inferColumns(rows);
+        return preformatBody(rows, columns).join("\n");
+      }
+    
+      function formatRows(rows) {
+        return rows.map(formatRow).join("\n");
+      }
+    
+      function formatRow(row) {
+        return row.map(formatValue).join(delimiter);
+      }
+    
+      function formatValue(value) {
+        return value == null ? ""
+            : value instanceof Date ? formatDate(value)
+            : reFormat.test(value += "") ? "\"" + value.replace(/"/g, "\"\"") + "\""
+            : value;
+      }
+    
+      return {
+        parse: parse,
+        parseRows: parseRows,
+        format: format,
+        formatBody: formatBody,
+        formatRows: formatRows,
+        formatRow: formatRow,
+        formatValue: formatValue
+      };
+    }
+    
+    var csv$1 = dsvFormat(",");
+    
+    var csvParse = csv$1.parse;
+    var csvParseRows = csv$1.parseRows;
+    var csvFormat = csv$1.format;
+    var csvFormatBody = csv$1.formatBody;
+    var csvFormatRows = csv$1.formatRows;
+    var csvFormatRow = csv$1.formatRow;
+    var csvFormatValue = csv$1.formatValue;
+    
+    var tsv$1 = dsvFormat("\t");
+    
+    var tsvParse = tsv$1.parse;
+    var tsvParseRows = tsv$1.parseRows;
+    var tsvFormat = tsv$1.format;
+    var tsvFormatBody = tsv$1.formatBody;
+    var tsvFormatRows = tsv$1.formatRows;
+    var tsvFormatRow = tsv$1.formatRow;
+    var tsvFormatValue = tsv$1.formatValue;
+    
+    function autoType(object) {
+      for (var key in object) {
+        var value = object[key].trim(), number, m;
+        if (!value) value = null;
+        else if (value === "true") value = true;
+        else if (value === "false") value = false;
+        else if (value === "NaN") value = NaN;
+        else if (!isNaN(number = +value)) value = number;
+        else if (m = value.match(/^([-+]\d{2})?\d{4}(-\d{2}(-\d{2})?)?(T\d{2}:\d{2}(:\d{2}(\.\d{3})?)?(Z|[-+]\d{2}:\d{2})?)?$/)) {
+          if (fixtz && !!m[4] && !m[7]) value = value.replace(/-/g, "/").replace(/T/, " ");
+          value = new Date(value);
+        }
+        else continue;
+        object[key] = value;
+      }
+      return object;
+    }
+    
+    // https://github.com/d3/d3-dsv/issues/45
+    const fixtz = new Date("2019-01-01T00:00").getHours() || new Date("2019-07-01T00:00").getHours();
+    
+    function responseBlob(response) {
+      if (!response.ok) throw new Error(response.status + " " + response.statusText);
+      return response.blob();
+    }
+    
+    function blob(input, init) {
+      return fetch(input, init).then(responseBlob);
+    }
+    
+    function responseArrayBuffer(response) {
+      if (!response.ok) throw new Error(response.status + " " + response.statusText);
+      return response.arrayBuffer();
+    }
+    
+    function buffer(input, init) {
+      return fetch(input, init).then(responseArrayBuffer);
+    }
+    
+    function responseText(response) {
+      if (!response.ok) throw new Error(response.status + " " + response.statusText);
+      return response.text();
+    }
+    
+    function text(input, init) {
+      return fetch(input, init).then(responseText);
+    }
+    
+    function dsvParse(parse) {
+      return function(input, init, row) {
+        if (arguments.length === 2 && typeof init === "function") row = init, init = undefined;
+        return text(input, init).then(function(response) {
+          return parse(response, row);
+        });
+      };
+    }
+    
+    function dsv(delimiter, input, init, row) {
+      if (arguments.length === 3 && typeof init === "function") row = init, init = undefined;
+      var format = dsvFormat(delimiter);
+      return text(input, init).then(function(response) {
+        return format.parse(response, row);
+      });
+    }
+    
+    var csv = dsvParse(csvParse);
+    var tsv = dsvParse(tsvParse);
+    
+    function image(input, init) {
+      return new Promise(function(resolve, reject) {
+        var image = new Image;
+        for (var key in init) image[key] = init[key];
+        image.onerror = reject;
+        image.onload = function() { resolve(image); };
+        image.src = input;
+      });
+    }
+    
+    function responseJson(response) {
+      if (!response.ok) throw new Error(response.status + " " + response.statusText);
+      if (response.status === 204 || response.status === 205) return;
+      return response.json();
+    }
+    
+    function json(input, init) {
+      return fetch(input, init).then(responseJson);
+    }
+    
+    function parser(type) {
+      return (input, init) => text(input, init)
+        .then(text => (new DOMParser).parseFromString(text, type));
+    }
+    
+    var xml = parser("application/xml");
+    
+    var html = parser("text/html");
+    
+    var svg = parser("image/svg+xml");
+    
+    function center(x, y) {
+      var nodes, strength = 1;
+    
+      if (x == null) x = 0;
+      if (y == null) y = 0;
+    
+      function force() {
+        var i,
+            n = nodes.length,
+            node,
+            sx = 0,
+            sy = 0;
+    
+        for (i = 0; i < n; ++i) {
+          node = nodes[i], sx += node.x, sy += node.y;
+        }
+    
+        for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, i = 0; i < n; ++i) {
+          node = nodes[i], node.x -= sx, node.y -= sy;
+        }
+      }
+    
+      force.initialize = function(_) {
+        nodes = _;
+      };
+    
+      force.x = function(_) {
+        return arguments.length ? (x = +_, force) : x;
+      };
+    
+      force.y = function(_) {
+        return arguments.length ? (y = +_, force) : y;
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = +_, force) : strength;
+      };
+    
+      return force;
+    }
+    
+    function tree_add(d) {
+      const x = +this._x.call(null, d),
+          y = +this._y.call(null, d);
+      return add(this.cover(x, y), x, y, d);
+    }
+    
+    function add(tree, x, y, d) {
+      if (isNaN(x) || isNaN(y)) return tree; // ignore invalid points
+    
+      var parent,
+          node = tree._root,
+          leaf = {data: d},
+          x0 = tree._x0,
+          y0 = tree._y0,
+          x1 = tree._x1,
+          y1 = tree._y1,
+          xm,
+          ym,
+          xp,
+          yp,
+          right,
+          bottom,
+          i,
+          j;
+    
+      // If the tree is empty, initialize the root as a leaf.
+      if (!node) return tree._root = leaf, tree;
+    
+      // Find the existing leaf for the new point, or add it.
+      while (node.length) {
+        if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+        if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+        if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;
+      }
+    
+      // Is the new point is exactly coincident with the existing point?
+      xp = +tree._x.call(null, node.data);
+      yp = +tree._y.call(null, node.data);
+      if (x === xp && y === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;
+    
+      // Otherwise, split the leaf node until the old and new point are separated.
+      do {
+        parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);
+        if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+        if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+      } while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm)));
+      return parent[j] = node, parent[i] = leaf, tree;
+    }
+    
+    function addAll(data) {
+      var d, i, n = data.length,
+          x,
+          y,
+          xz = new Array(n),
+          yz = new Array(n),
+          x0 = Infinity,
+          y0 = Infinity,
+          x1 = -Infinity,
+          y1 = -Infinity;
+    
+      // Compute the points and their extent.
+      for (i = 0; i < n; ++i) {
+        if (isNaN(x = +this._x.call(null, d = data[i])) || isNaN(y = +this._y.call(null, d))) continue;
+        xz[i] = x;
+        yz[i] = y;
+        if (x < x0) x0 = x;
+        if (x > x1) x1 = x;
+        if (y < y0) y0 = y;
+        if (y > y1) y1 = y;
+      }
+    
+      // If there were no (valid) points, abort.
+      if (x0 > x1 || y0 > y1) return this;
+    
+      // Expand the tree to cover the new points.
+      this.cover(x0, y0).cover(x1, y1);
+    
+      // Add the new points.
+      for (i = 0; i < n; ++i) {
+        add(this, xz[i], yz[i], data[i]);
+      }
+    
+      return this;
+    }
+    
+    function tree_cover(x, y) {
+      if (isNaN(x = +x) || isNaN(y = +y)) return this; // ignore invalid points
+    
+      var x0 = this._x0,
+          y0 = this._y0,
+          x1 = this._x1,
+          y1 = this._y1;
+    
+      // If the quadtree has no extent, initialize them.
+      // Integer extent are necessary so that if we later double the extent,
+      // the existing quadrant boundaries don’t change due to floating point error!
+      if (isNaN(x0)) {
+        x1 = (x0 = Math.floor(x)) + 1;
+        y1 = (y0 = Math.floor(y)) + 1;
+      }
+    
+      // Otherwise, double repeatedly to cover.
+      else {
+        var z = x1 - x0 || 1,
+            node = this._root,
+            parent,
+            i;
+    
+        while (x0 > x || x >= x1 || y0 > y || y >= y1) {
+          i = (y < y0) << 1 | (x < x0);
+          parent = new Array(4), parent[i] = node, node = parent, z *= 2;
+          switch (i) {
+            case 0: x1 = x0 + z, y1 = y0 + z; break;
+            case 1: x0 = x1 - z, y1 = y0 + z; break;
+            case 2: x1 = x0 + z, y0 = y1 - z; break;
+            case 3: x0 = x1 - z, y0 = y1 - z; break;
+          }
+        }
+    
+        if (this._root && this._root.length) this._root = node;
+      }
+    
+      this._x0 = x0;
+      this._y0 = y0;
+      this._x1 = x1;
+      this._y1 = y1;
+      return this;
+    }
+    
+    function tree_data() {
+      var data = [];
+      this.visit(function(node) {
+        if (!node.length) do data.push(node.data); while (node = node.next)
+      });
+      return data;
+    }
+    
+    function tree_extent(_) {
+      return arguments.length
+          ? this.cover(+_[0][0], +_[0][1]).cover(+_[1][0], +_[1][1])
+          : isNaN(this._x0) ? undefined : [[this._x0, this._y0], [this._x1, this._y1]];
+    }
+    
+    function Quad(node, x0, y0, x1, y1) {
+      this.node = node;
+      this.x0 = x0;
+      this.y0 = y0;
+      this.x1 = x1;
+      this.y1 = y1;
+    }
+    
+    function tree_find(x, y, radius) {
+      var data,
+          x0 = this._x0,
+          y0 = this._y0,
+          x1,
+          y1,
+          x2,
+          y2,
+          x3 = this._x1,
+          y3 = this._y1,
+          quads = [],
+          node = this._root,
+          q,
+          i;
+    
+      if (node) quads.push(new Quad(node, x0, y0, x3, y3));
+      if (radius == null) radius = Infinity;
+      else {
+        x0 = x - radius, y0 = y - radius;
+        x3 = x + radius, y3 = y + radius;
+        radius *= radius;
+      }
+    
+      while (q = quads.pop()) {
+    
+        // Stop searching if this quadrant can’t contain a closer node.
+        if (!(node = q.node)
+            || (x1 = q.x0) > x3
+            || (y1 = q.y0) > y3
+            || (x2 = q.x1) < x0
+            || (y2 = q.y1) < y0) continue;
+    
+        // Bisect the current quadrant.
+        if (node.length) {
+          var xm = (x1 + x2) / 2,
+              ym = (y1 + y2) / 2;
+    
+          quads.push(
+            new Quad(node[3], xm, ym, x2, y2),
+            new Quad(node[2], x1, ym, xm, y2),
+            new Quad(node[1], xm, y1, x2, ym),
+            new Quad(node[0], x1, y1, xm, ym)
+          );
+    
+          // Visit the closest quadrant first.
+          if (i = (y >= ym) << 1 | (x >= xm)) {
+            q = quads[quads.length - 1];
+            quads[quads.length - 1] = quads[quads.length - 1 - i];
+            quads[quads.length - 1 - i] = q;
+          }
+        }
+    
+        // Visit this point. (Visiting coincident points isn’t necessary!)
+        else {
+          var dx = x - +this._x.call(null, node.data),
+              dy = y - +this._y.call(null, node.data),
+              d2 = dx * dx + dy * dy;
+          if (d2 < radius) {
+            var d = Math.sqrt(radius = d2);
+            x0 = x - d, y0 = y - d;
+            x3 = x + d, y3 = y + d;
+            data = node.data;
+          }
+        }
+      }
+    
+      return data;
+    }
+    
+    function tree_remove(d) {
+      if (isNaN(x = +this._x.call(null, d)) || isNaN(y = +this._y.call(null, d))) return this; // ignore invalid points
+    
+      var parent,
+          node = this._root,
+          retainer,
+          previous,
+          next,
+          x0 = this._x0,
+          y0 = this._y0,
+          x1 = this._x1,
+          y1 = this._y1,
+          x,
+          y,
+          xm,
+          ym,
+          right,
+          bottom,
+          i,
+          j;
+    
+      // If the tree is empty, initialize the root as a leaf.
+      if (!node) return this;
+    
+      // Find the leaf node for the point.
+      // While descending, also retain the deepest parent with a non-removed sibling.
+      if (node.length) while (true) {
+        if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
+        if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
+        if (!(parent = node, node = node[i = bottom << 1 | right])) return this;
+        if (!node.length) break;
+        if (parent[(i + 1) & 3] || parent[(i + 2) & 3] || parent[(i + 3) & 3]) retainer = parent, j = i;
+      }
+    
+      // Find the point to remove.
+      while (node.data !== d) if (!(previous = node, node = node.next)) return this;
+      if (next = node.next) delete node.next;
+    
+      // If there are multiple coincident points, remove just the point.
+      if (previous) return (next ? previous.next = next : delete previous.next), this;
+    
+      // If this is the root point, remove it.
+      if (!parent) return this._root = next, this;
+    
+      // Remove this leaf.
+      next ? parent[i] = next : delete parent[i];
+    
+      // If the parent now contains exactly one leaf, collapse superfluous parents.
+      if ((node = parent[0] || parent[1] || parent[2] || parent[3])
+          && node === (parent[3] || parent[2] || parent[1] || parent[0])
+          && !node.length) {
+        if (retainer) retainer[j] = node;
+        else this._root = node;
+      }
+    
+      return this;
+    }
+    
+    function removeAll(data) {
+      for (var i = 0, n = data.length; i < n; ++i) this.remove(data[i]);
+      return this;
+    }
+    
+    function tree_root() {
+      return this._root;
+    }
+    
+    function tree_size() {
+      var size = 0;
+      this.visit(function(node) {
+        if (!node.length) do ++size; while (node = node.next)
+      });
+      return size;
+    }
+    
+    function tree_visit(callback) {
+      var quads = [], q, node = this._root, child, x0, y0, x1, y1;
+      if (node) quads.push(new Quad(node, this._x0, this._y0, this._x1, this._y1));
+      while (q = quads.pop()) {
+        if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1) && node.length) {
+          var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
+          if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
+          if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
+          if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
+          if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
+        }
+      }
+      return this;
+    }
+    
+    function tree_visitAfter(callback) {
+      var quads = [], next = [], q;
+      if (this._root) quads.push(new Quad(this._root, this._x0, this._y0, this._x1, this._y1));
+      while (q = quads.pop()) {
+        var node = q.node;
+        if (node.length) {
+          var child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
+          if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
+          if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
+          if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
+          if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
+        }
+        next.push(q);
+      }
+      while (q = next.pop()) {
+        callback(q.node, q.x0, q.y0, q.x1, q.y1);
+      }
+      return this;
+    }
+    
+    function defaultX(d) {
+      return d[0];
+    }
+    
+    function tree_x(_) {
+      return arguments.length ? (this._x = _, this) : this._x;
+    }
+    
+    function defaultY(d) {
+      return d[1];
+    }
+    
+    function tree_y(_) {
+      return arguments.length ? (this._y = _, this) : this._y;
+    }
+    
+    function quadtree(nodes, x, y) {
+      var tree = new Quadtree(x == null ? defaultX : x, y == null ? defaultY : y, NaN, NaN, NaN, NaN);
+      return nodes == null ? tree : tree.addAll(nodes);
+    }
+    
+    function Quadtree(x, y, x0, y0, x1, y1) {
+      this._x = x;
+      this._y = y;
+      this._x0 = x0;
+      this._y0 = y0;
+      this._x1 = x1;
+      this._y1 = y1;
+      this._root = undefined;
+    }
+    
+    function leaf_copy(leaf) {
+      var copy = {data: leaf.data}, next = copy;
+      while (leaf = leaf.next) next = next.next = {data: leaf.data};
+      return copy;
+    }
+    
+    var treeProto = quadtree.prototype = Quadtree.prototype;
+    
+    treeProto.copy = function() {
+      var copy = new Quadtree(this._x, this._y, this._x0, this._y0, this._x1, this._y1),
+          node = this._root,
+          nodes,
+          child;
+    
+      if (!node) return copy;
+    
+      if (!node.length) return copy._root = leaf_copy(node), copy;
+    
+      nodes = [{source: node, target: copy._root = new Array(4)}];
+      while (node = nodes.pop()) {
+        for (var i = 0; i < 4; ++i) {
+          if (child = node.source[i]) {
+            if (child.length) nodes.push({source: child, target: node.target[i] = new Array(4)});
+            else node.target[i] = leaf_copy(child);
+          }
+        }
+      }
+    
+      return copy;
+    };
+    
+    treeProto.add = tree_add;
+    treeProto.addAll = addAll;
+    treeProto.cover = tree_cover;
+    treeProto.data = tree_data;
+    treeProto.extent = tree_extent;
+    treeProto.find = tree_find;
+    treeProto.remove = tree_remove;
+    treeProto.removeAll = removeAll;
+    treeProto.root = tree_root;
+    treeProto.size = tree_size;
+    treeProto.visit = tree_visit;
+    treeProto.visitAfter = tree_visitAfter;
+    treeProto.x = tree_x;
+    treeProto.y = tree_y;
+    
+    function constant$4(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function jiggle(random) {
+      return (random() - 0.5) * 1e-6;
+    }
+    
+    function x$3(d) {
+      return d.x + d.vx;
+    }
+    
+    function y$3(d) {
+      return d.y + d.vy;
+    }
+    
+    function collide(radius) {
+      var nodes,
+          radii,
+          random,
+          strength = 1,
+          iterations = 1;
+    
+      if (typeof radius !== "function") radius = constant$4(radius == null ? 1 : +radius);
+    
+      function force() {
+        var i, n = nodes.length,
+            tree,
+            node,
+            xi,
+            yi,
+            ri,
+            ri2;
+    
+        for (var k = 0; k < iterations; ++k) {
+          tree = quadtree(nodes, x$3, y$3).visitAfter(prepare);
+          for (i = 0; i < n; ++i) {
+            node = nodes[i];
+            ri = radii[node.index], ri2 = ri * ri;
+            xi = node.x + node.vx;
+            yi = node.y + node.vy;
+            tree.visit(apply);
+          }
+        }
+    
+        function apply(quad, x0, y0, x1, y1) {
+          var data = quad.data, rj = quad.r, r = ri + rj;
+          if (data) {
+            if (data.index > node.index) {
+              var x = xi - data.x - data.vx,
+                  y = yi - data.y - data.vy,
+                  l = x * x + y * y;
+              if (l < r * r) {
+                if (x === 0) x = jiggle(random), l += x * x;
+                if (y === 0) y = jiggle(random), l += y * y;
+                l = (r - (l = Math.sqrt(l))) / l * strength;
+                node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
+                node.vy += (y *= l) * r;
+                data.vx -= x * (r = 1 - r);
+                data.vy -= y * r;
+              }
+            }
+            return;
+          }
+          return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
+        }
+      }
+    
+      function prepare(quad) {
+        if (quad.data) return quad.r = radii[quad.data.index];
+        for (var i = quad.r = 0; i < 4; ++i) {
+          if (quad[i] && quad[i].r > quad.r) {
+            quad.r = quad[i].r;
+          }
+        }
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+        var i, n = nodes.length, node;
+        radii = new Array(n);
+        for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
+      }
+    
+      force.initialize = function(_nodes, _random) {
+        nodes = _nodes;
+        random = _random;
+        initialize();
+      };
+    
+      force.iterations = function(_) {
+        return arguments.length ? (iterations = +_, force) : iterations;
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = +_, force) : strength;
+      };
+    
+      force.radius = function(_) {
+        return arguments.length ? (radius = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : radius;
+      };
+    
+      return force;
+    }
+    
+    function index$3(d) {
+      return d.index;
+    }
+    
+    function find(nodeById, nodeId) {
+      var node = nodeById.get(nodeId);
+      if (!node) throw new Error("node not found: " + nodeId);
+      return node;
+    }
+    
+    function link$2(links) {
+      var id = index$3,
+          strength = defaultStrength,
+          strengths,
+          distance = constant$4(30),
+          distances,
+          nodes,
+          count,
+          bias,
+          random,
+          iterations = 1;
+    
+      if (links == null) links = [];
+    
+      function defaultStrength(link) {
+        return 1 / Math.min(count[link.source.index], count[link.target.index]);
+      }
+    
+      function force(alpha) {
+        for (var k = 0, n = links.length; k < iterations; ++k) {
+          for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
+            link = links[i], source = link.source, target = link.target;
+            x = target.x + target.vx - source.x - source.vx || jiggle(random);
+            y = target.y + target.vy - source.y - source.vy || jiggle(random);
+            l = Math.sqrt(x * x + y * y);
+            l = (l - distances[i]) / l * alpha * strengths[i];
+            x *= l, y *= l;
+            target.vx -= x * (b = bias[i]);
+            target.vy -= y * b;
+            source.vx += x * (b = 1 - b);
+            source.vy += y * b;
+          }
+        }
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+    
+        var i,
+            n = nodes.length,
+            m = links.length,
+            nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])),
+            link;
+    
+        for (i = 0, count = new Array(n); i < m; ++i) {
+          link = links[i], link.index = i;
+          if (typeof link.source !== "object") link.source = find(nodeById, link.source);
+          if (typeof link.target !== "object") link.target = find(nodeById, link.target);
+          count[link.source.index] = (count[link.source.index] || 0) + 1;
+          count[link.target.index] = (count[link.target.index] || 0) + 1;
+        }
+    
+        for (i = 0, bias = new Array(m); i < m; ++i) {
+          link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
+        }
+    
+        strengths = new Array(m), initializeStrength();
+        distances = new Array(m), initializeDistance();
+      }
+    
+      function initializeStrength() {
+        if (!nodes) return;
+    
+        for (var i = 0, n = links.length; i < n; ++i) {
+          strengths[i] = +strength(links[i], i, links);
+        }
+      }
+    
+      function initializeDistance() {
+        if (!nodes) return;
+    
+        for (var i = 0, n = links.length; i < n; ++i) {
+          distances[i] = +distance(links[i], i, links);
+        }
+      }
+    
+      force.initialize = function(_nodes, _random) {
+        nodes = _nodes;
+        random = _random;
+        initialize();
+      };
+    
+      force.links = function(_) {
+        return arguments.length ? (links = _, initialize(), force) : links;
+      };
+    
+      force.id = function(_) {
+        return arguments.length ? (id = _, force) : id;
+      };
+    
+      force.iterations = function(_) {
+        return arguments.length ? (iterations = +_, force) : iterations;
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = typeof _ === "function" ? _ : constant$4(+_), initializeStrength(), force) : strength;
+      };
+    
+      force.distance = function(_) {
+        return arguments.length ? (distance = typeof _ === "function" ? _ : constant$4(+_), initializeDistance(), force) : distance;
+      };
+    
+      return force;
+    }
+    
+    // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
+    const a$1 = 1664525;
+    const c$3 = 1013904223;
+    const m = 4294967296; // 2^32
+    
+    function lcg$1() {
+      let s = 1;
+      return () => (s = (a$1 * s + c$3) % m) / m;
+    }
+    
+    function x$2(d) {
+      return d.x;
+    }
+    
+    function y$2(d) {
+      return d.y;
+    }
+    
+    var initialRadius = 10,
+        initialAngle = Math.PI * (3 - Math.sqrt(5));
+    
+    function simulation(nodes) {
+      var simulation,
+          alpha = 1,
+          alphaMin = 0.001,
+          alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
+          alphaTarget = 0,
+          velocityDecay = 0.6,
+          forces = new Map(),
+          stepper = timer(step),
+          event = dispatch("tick", "end"),
+          random = lcg$1();
+    
+      if (nodes == null) nodes = [];
+    
+      function step() {
+        tick();
+        event.call("tick", simulation);
+        if (alpha < alphaMin) {
+          stepper.stop();
+          event.call("end", simulation);
+        }
+      }
+    
+      function tick(iterations) {
+        var i, n = nodes.length, node;
+    
+        if (iterations === undefined) iterations = 1;
+    
+        for (var k = 0; k < iterations; ++k) {
+          alpha += (alphaTarget - alpha) * alphaDecay;
+    
+          forces.forEach(function(force) {
+            force(alpha);
+          });
+    
+          for (i = 0; i < n; ++i) {
+            node = nodes[i];
+            if (node.fx == null) node.x += node.vx *= velocityDecay;
+            else node.x = node.fx, node.vx = 0;
+            if (node.fy == null) node.y += node.vy *= velocityDecay;
+            else node.y = node.fy, node.vy = 0;
+          }
+        }
+    
+        return simulation;
+      }
+    
+      function initializeNodes() {
+        for (var i = 0, n = nodes.length, node; i < n; ++i) {
+          node = nodes[i], node.index = i;
+          if (node.fx != null) node.x = node.fx;
+          if (node.fy != null) node.y = node.fy;
+          if (isNaN(node.x) || isNaN(node.y)) {
+            var radius = initialRadius * Math.sqrt(0.5 + i), angle = i * initialAngle;
+            node.x = radius * Math.cos(angle);
+            node.y = radius * Math.sin(angle);
+          }
+          if (isNaN(node.vx) || isNaN(node.vy)) {
+            node.vx = node.vy = 0;
+          }
+        }
+      }
+    
+      function initializeForce(force) {
+        if (force.initialize) force.initialize(nodes, random);
+        return force;
+      }
+    
+      initializeNodes();
+    
+      return simulation = {
+        tick: tick,
+    
+        restart: function() {
+          return stepper.restart(step), simulation;
+        },
+    
+        stop: function() {
+          return stepper.stop(), simulation;
+        },
+    
+        nodes: function(_) {
+          return arguments.length ? (nodes = _, initializeNodes(), forces.forEach(initializeForce), simulation) : nodes;
+        },
+    
+        alpha: function(_) {
+          return arguments.length ? (alpha = +_, simulation) : alpha;
+        },
+    
+        alphaMin: function(_) {
+          return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
+        },
+    
+        alphaDecay: function(_) {
+          return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
+        },
+    
+        alphaTarget: function(_) {
+          return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
+        },
+    
+        velocityDecay: function(_) {
+          return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
+        },
+    
+        randomSource: function(_) {
+          return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random;
+        },
+    
+        force: function(name, _) {
+          return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
+        },
+    
+        find: function(x, y, radius) {
+          var i = 0,
+              n = nodes.length,
+              dx,
+              dy,
+              d2,
+              node,
+              closest;
+    
+          if (radius == null) radius = Infinity;
+          else radius *= radius;
+    
+          for (i = 0; i < n; ++i) {
+            node = nodes[i];
+            dx = x - node.x;
+            dy = y - node.y;
+            d2 = dx * dx + dy * dy;
+            if (d2 < radius) closest = node, radius = d2;
+          }
+    
+          return closest;
+        },
+    
+        on: function(name, _) {
+          return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
+        }
+      };
+    }
+    
+    function manyBody() {
+      var nodes,
+          node,
+          random,
+          alpha,
+          strength = constant$4(-30),
+          strengths,
+          distanceMin2 = 1,
+          distanceMax2 = Infinity,
+          theta2 = 0.81;
+    
+      function force(_) {
+        var i, n = nodes.length, tree = quadtree(nodes, x$2, y$2).visitAfter(accumulate);
+        for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+        var i, n = nodes.length, node;
+        strengths = new Array(n);
+        for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes);
+      }
+    
+      function accumulate(quad) {
+        var strength = 0, q, c, weight = 0, x, y, i;
+    
+        // For internal nodes, accumulate forces from child quadrants.
+        if (quad.length) {
+          for (x = y = i = 0; i < 4; ++i) {
+            if ((q = quad[i]) && (c = Math.abs(q.value))) {
+              strength += q.value, weight += c, x += c * q.x, y += c * q.y;
+            }
+          }
+          quad.x = x / weight;
+          quad.y = y / weight;
+        }
+    
+        // For leaf nodes, accumulate forces from coincident quadrants.
+        else {
+          q = quad;
+          q.x = q.data.x;
+          q.y = q.data.y;
+          do strength += strengths[q.data.index];
+          while (q = q.next);
+        }
+    
+        quad.value = strength;
+      }
+    
+      function apply(quad, x1, _, x2) {
+        if (!quad.value) return true;
+    
+        var x = quad.x - node.x,
+            y = quad.y - node.y,
+            w = x2 - x1,
+            l = x * x + y * y;
+    
+        // Apply the Barnes-Hut approximation if possible.
+        // Limit forces for very close nodes; randomize direction if coincident.
+        if (w * w / theta2 < l) {
+          if (l < distanceMax2) {
+            if (x === 0) x = jiggle(random), l += x * x;
+            if (y === 0) y = jiggle(random), l += y * y;
+            if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
+            node.vx += x * quad.value * alpha / l;
+            node.vy += y * quad.value * alpha / l;
+          }
+          return true;
+        }
+    
+        // Otherwise, process points directly.
+        else if (quad.length || l >= distanceMax2) return;
+    
+        // Limit forces for very close nodes; randomize direction if coincident.
+        if (quad.data !== node || quad.next) {
+          if (x === 0) x = jiggle(random), l += x * x;
+          if (y === 0) y = jiggle(random), l += y * y;
+          if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
+        }
+    
+        do if (quad.data !== node) {
+          w = strengths[quad.data.index] * alpha / l;
+          node.vx += x * w;
+          node.vy += y * w;
+        } while (quad = quad.next);
+      }
+    
+      force.initialize = function(_nodes, _random) {
+        nodes = _nodes;
+        random = _random;
+        initialize();
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : strength;
+      };
+    
+      force.distanceMin = function(_) {
+        return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
+      };
+    
+      force.distanceMax = function(_) {
+        return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
+      };
+    
+      force.theta = function(_) {
+        return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
+      };
+    
+      return force;
+    }
+    
+    function radial$1(radius, x, y) {
+      var nodes,
+          strength = constant$4(0.1),
+          strengths,
+          radiuses;
+    
+      if (typeof radius !== "function") radius = constant$4(+radius);
+      if (x == null) x = 0;
+      if (y == null) y = 0;
+    
+      function force(alpha) {
+        for (var i = 0, n = nodes.length; i < n; ++i) {
+          var node = nodes[i],
+              dx = node.x - x || 1e-6,
+              dy = node.y - y || 1e-6,
+              r = Math.sqrt(dx * dx + dy * dy),
+              k = (radiuses[i] - r) * strengths[i] * alpha / r;
+          node.vx += dx * k;
+          node.vy += dy * k;
+        }
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+        var i, n = nodes.length;
+        strengths = new Array(n);
+        radiuses = new Array(n);
+        for (i = 0; i < n; ++i) {
+          radiuses[i] = +radius(nodes[i], i, nodes);
+          strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes);
+        }
+      }
+    
+      force.initialize = function(_) {
+        nodes = _, initialize();
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : strength;
+      };
+    
+      force.radius = function(_) {
+        return arguments.length ? (radius = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : radius;
+      };
+    
+      force.x = function(_) {
+        return arguments.length ? (x = +_, force) : x;
+      };
+    
+      force.y = function(_) {
+        return arguments.length ? (y = +_, force) : y;
+      };
+    
+      return force;
+    }
+    
+    function x$1(x) {
+      var strength = constant$4(0.1),
+          nodes,
+          strengths,
+          xz;
+    
+      if (typeof x !== "function") x = constant$4(x == null ? 0 : +x);
+    
+      function force(alpha) {
+        for (var i = 0, n = nodes.length, node; i < n; ++i) {
+          node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
+        }
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+        var i, n = nodes.length;
+        strengths = new Array(n);
+        xz = new Array(n);
+        for (i = 0; i < n; ++i) {
+          strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
+        }
+      }
+    
+      force.initialize = function(_) {
+        nodes = _;
+        initialize();
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : strength;
+      };
+    
+      force.x = function(_) {
+        return arguments.length ? (x = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : x;
+      };
+    
+      return force;
+    }
+    
+    function y$1(y) {
+      var strength = constant$4(0.1),
+          nodes,
+          strengths,
+          yz;
+    
+      if (typeof y !== "function") y = constant$4(y == null ? 0 : +y);
+    
+      function force(alpha) {
+        for (var i = 0, n = nodes.length, node; i < n; ++i) {
+          node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
+        }
+      }
+    
+      function initialize() {
+        if (!nodes) return;
+        var i, n = nodes.length;
+        strengths = new Array(n);
+        yz = new Array(n);
+        for (i = 0; i < n; ++i) {
+          strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
+        }
+      }
+    
+      force.initialize = function(_) {
+        nodes = _;
+        initialize();
+      };
+    
+      force.strength = function(_) {
+        return arguments.length ? (strength = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : strength;
+      };
+    
+      force.y = function(_) {
+        return arguments.length ? (y = typeof _ === "function" ? _ : constant$4(+_), initialize(), force) : y;
+      };
+    
+      return force;
+    }
+    
+    function formatDecimal(x) {
+      return Math.abs(x = Math.round(x)) >= 1e21
+          ? x.toLocaleString("en").replace(/,/g, "")
+          : x.toString(10);
+    }
+    
+    // Computes the decimal coefficient and exponent of the specified number x with
+    // significant digits p, where x is positive and p is in [1, 21] or undefined.
+    // For example, formatDecimalParts(1.23) returns ["123", 0].
+    function formatDecimalParts(x, p) {
+      if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity
+      var i, coefficient = x.slice(0, i);
+    
+      // The string returned by toExponential either has the form \d\.\d+e[-+]\d+
+      // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3).
+      return [
+        coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient,
+        +x.slice(i + 1)
+      ];
+    }
+    
+    function exponent(x) {
+      return x = formatDecimalParts(Math.abs(x)), x ? x[1] : NaN;
+    }
+    
+    function formatGroup(grouping, thousands) {
+      return function(value, width) {
+        var i = value.length,
+            t = [],
+            j = 0,
+            g = grouping[0],
+            length = 0;
+    
+        while (i > 0 && g > 0) {
+          if (length + g + 1 > width) g = Math.max(1, width - length);
+          t.push(value.substring(i -= g, i + g));
+          if ((length += g + 1) > width) break;
+          g = grouping[j = (j + 1) % grouping.length];
+        }
+    
+        return t.reverse().join(thousands);
+      };
+    }
+    
+    function formatNumerals(numerals) {
+      return function(value) {
+        return value.replace(/[0-9]/g, function(i) {
+          return numerals[+i];
+        });
+      };
+    }
+    
+    // [[fill]align][sign][symbol][0][width][,][.precision][~][type]
+    var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;
+    
+    function formatSpecifier(specifier) {
+      if (!(match = re.exec(specifier))) throw new Error("invalid format: " + specifier);
+      var match;
+      return new FormatSpecifier({
+        fill: match[1],
+        align: match[2],
+        sign: match[3],
+        symbol: match[4],
+        zero: match[5],
+        width: match[6],
+        comma: match[7],
+        precision: match[8] && match[8].slice(1),
+        trim: match[9],
+        type: match[10]
+      });
+    }
+    
+    formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof
+    
+    function FormatSpecifier(specifier) {
+      this.fill = specifier.fill === undefined ? " " : specifier.fill + "";
+      this.align = specifier.align === undefined ? ">" : specifier.align + "";
+      this.sign = specifier.sign === undefined ? "-" : specifier.sign + "";
+      this.symbol = specifier.symbol === undefined ? "" : specifier.symbol + "";
+      this.zero = !!specifier.zero;
+      this.width = specifier.width === undefined ? undefined : +specifier.width;
+      this.comma = !!specifier.comma;
+      this.precision = specifier.precision === undefined ? undefined : +specifier.precision;
+      this.trim = !!specifier.trim;
+      this.type = specifier.type === undefined ? "" : specifier.type + "";
+    }
+    
+    FormatSpecifier.prototype.toString = function() {
+      return this.fill
+          + this.align
+          + this.sign
+          + this.symbol
+          + (this.zero ? "0" : "")
+          + (this.width === undefined ? "" : Math.max(1, this.width | 0))
+          + (this.comma ? "," : "")
+          + (this.precision === undefined ? "" : "." + Math.max(0, this.precision | 0))
+          + (this.trim ? "~" : "")
+          + this.type;
+    };
+    
+    // Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.
+    function formatTrim(s) {
+      out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {
+        switch (s[i]) {
+          case ".": i0 = i1 = i; break;
+          case "0": if (i0 === 0) i0 = i; i1 = i; break;
+          default: if (!+s[i]) break out; if (i0 > 0) i0 = 0; break;
+        }
+      }
+      return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;
+    }
+    
+    var prefixExponent;
+    
+    function formatPrefixAuto(x, p) {
+      var d = formatDecimalParts(x, p);
+      if (!d) return x + "";
+      var coefficient = d[0],
+          exponent = d[1],
+          i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,
+          n = coefficient.length;
+      return i === n ? coefficient
+          : i > n ? coefficient + new Array(i - n + 1).join("0")
+          : i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i)
+          : "0." + new Array(1 - i).join("0") + formatDecimalParts(x, Math.max(0, p + i - 1))[0]; // less than 1y!
+    }
+    
+    function formatRounded(x, p) {
+      var d = formatDecimalParts(x, p);
+      if (!d) return x + "";
+      var coefficient = d[0],
+          exponent = d[1];
+      return exponent < 0 ? "0." + new Array(-exponent).join("0") + coefficient
+          : coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + "." + coefficient.slice(exponent + 1)
+          : coefficient + new Array(exponent - coefficient.length + 2).join("0");
+    }
+    
+    var formatTypes = {
+      "%": (x, p) => (x * 100).toFixed(p),
+      "b": (x) => Math.round(x).toString(2),
+      "c": (x) => x + "",
+      "d": formatDecimal,
+      "e": (x, p) => x.toExponential(p),
+      "f": (x, p) => x.toFixed(p),
+      "g": (x, p) => x.toPrecision(p),
+      "o": (x) => Math.round(x).toString(8),
+      "p": (x, p) => formatRounded(x * 100, p),
+      "r": formatRounded,
+      "s": formatPrefixAuto,
+      "X": (x) => Math.round(x).toString(16).toUpperCase(),
+      "x": (x) => Math.round(x).toString(16)
+    };
+    
+    function identity$6(x) {
+      return x;
+    }
+    
+    var map = Array.prototype.map,
+        prefixes = ["y","z","a","f","p","n","\xB5","m","","k","M","G","T","P","E","Z","Y"];
+    
+    function formatLocale$1(locale) {
+      var group = locale.grouping === undefined || locale.thousands === undefined ? identity$6 : formatGroup(map.call(locale.grouping, Number), locale.thousands + ""),
+          currencyPrefix = locale.currency === undefined ? "" : locale.currency[0] + "",
+          currencySuffix = locale.currency === undefined ? "" : locale.currency[1] + "",
+          decimal = locale.decimal === undefined ? "." : locale.decimal + "",
+          numerals = locale.numerals === undefined ? identity$6 : formatNumerals(map.call(locale.numerals, String)),
+          percent = locale.percent === undefined ? "%" : locale.percent + "",
+          minus = locale.minus === undefined ? "\u2212" : locale.minus + "",
+          nan = locale.nan === undefined ? "NaN" : locale.nan + "";
+    
+      function newFormat(specifier) {
+        specifier = formatSpecifier(specifier);
+    
+        var fill = specifier.fill,
+            align = specifier.align,
+            sign = specifier.sign,
+            symbol = specifier.symbol,
+            zero = specifier.zero,
+            width = specifier.width,
+            comma = specifier.comma,
+            precision = specifier.precision,
+            trim = specifier.trim,
+            type = specifier.type;
+    
+        // The "n" type is an alias for ",g".
+        if (type === "n") comma = true, type = "g";
+    
+        // The "" type, and any invalid type, is an alias for ".12~g".
+        else if (!formatTypes[type]) precision === undefined && (precision = 12), trim = true, type = "g";
+    
+        // If zero fill is specified, padding goes after sign and before digits.
+        if (zero || (fill === "0" && align === "=")) zero = true, fill = "0", align = "=";
+    
+        // Compute the prefix and suffix.
+        // For SI-prefix, the suffix is lazily computed.
+        var prefix = symbol === "$" ? currencyPrefix : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "",
+            suffix = symbol === "$" ? currencySuffix : /[%p]/.test(type) ? percent : "";
+    
+        // What format function should we use?
+        // Is this an integer type?
+        // Can this type generate exponential notation?
+        var formatType = formatTypes[type],
+            maybeSuffix = /[defgprs%]/.test(type);
+    
+        // Set the default precision if not specified,
+        // or clamp the specified precision to the supported range.
+        // For significant precision, it must be in [1, 21].
+        // For fixed precision, it must be in [0, 20].
+        precision = precision === undefined ? 6
+            : /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))
+            : Math.max(0, Math.min(20, precision));
+    
+        function format(value) {
+          var valuePrefix = prefix,
+              valueSuffix = suffix,
+              i, n, c;
+    
+          if (type === "c") {
+            valueSuffix = formatType(value) + valueSuffix;
+            value = "";
+          } else {
+            value = +value;
+    
+            // Determine the sign. -0 is not less than 0, but 1 / -0 is!
+            var valueNegative = value < 0 || 1 / value < 0;
+    
+            // Perform the initial formatting.
+            value = isNaN(value) ? nan : formatType(Math.abs(value), precision);
+    
+            // Trim insignificant zeros.
+            if (trim) value = formatTrim(value);
+    
+            // If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign.
+            if (valueNegative && +value === 0 && sign !== "+") valueNegative = false;
+    
+            // Compute the prefix and suffix.
+            valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix;
+            valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : "");
+    
+            // Break the formatted value into the integer “value” part that can be
+            // grouped, and fractional or exponential “suffix” part that is not.
+            if (maybeSuffix) {
+              i = -1, n = value.length;
+              while (++i < n) {
+                if (c = value.charCodeAt(i), 48 > c || c > 57) {
+                  valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix;
+                  value = value.slice(0, i);
+                  break;
+                }
+              }
+            }
+          }
+    
+          // If the fill character is not "0", grouping is applied before padding.
+          if (comma && !zero) value = group(value, Infinity);
+    
+          // Compute the padding.
+          var length = valuePrefix.length + value.length + valueSuffix.length,
+              padding = length < width ? new Array(width - length + 1).join(fill) : "";
+    
+          // If the fill character is "0", grouping is applied after padding.
+          if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = "";
+    
+          // Reconstruct the final output based on the desired alignment.
+          switch (align) {
+            case "<": value = valuePrefix + value + valueSuffix + padding; break;
+            case "=": value = valuePrefix + padding + value + valueSuffix; break;
+            case "^": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break;
+            default: value = padding + valuePrefix + value + valueSuffix; break;
+          }
+    
+          return numerals(value);
+        }
+    
+        format.toString = function() {
+          return specifier + "";
+        };
+    
+        return format;
+      }
+    
+      function formatPrefix(specifier, value) {
+        var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
+            e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,
+            k = Math.pow(10, -e),
+            prefix = prefixes[8 + e / 3];
+        return function(value) {
+          return f(k * value) + prefix;
+        };
+      }
+    
+      return {
+        format: newFormat,
+        formatPrefix: formatPrefix
+      };
+    }
+    
+    var locale$1;
+    exports.format = void 0;
+    exports.formatPrefix = void 0;
+    
+    defaultLocale$1({
+      thousands: ",",
+      grouping: [3],
+      currency: ["$", ""]
+    });
+    
+    function defaultLocale$1(definition) {
+      locale$1 = formatLocale$1(definition);
+      exports.format = locale$1.format;
+      exports.formatPrefix = locale$1.formatPrefix;
+      return locale$1;
+    }
+    
+    function precisionFixed(step) {
+      return Math.max(0, -exponent(Math.abs(step)));
+    }
+    
+    function precisionPrefix(step, value) {
+      return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step)));
+    }
+    
+    function precisionRound(step, max) {
+      step = Math.abs(step), max = Math.abs(max) - step;
+      return Math.max(0, exponent(max) - exponent(step)) + 1;
+    }
+    
+    var epsilon$1 = 1e-6;
+    var epsilon2 = 1e-12;
+    var pi$1 = Math.PI;
+    var halfPi$1 = pi$1 / 2;
+    var quarterPi = pi$1 / 4;
+    var tau$1 = pi$1 * 2;
+    
+    var degrees = 180 / pi$1;
+    var radians = pi$1 / 180;
+    
+    var abs$1 = Math.abs;
+    var atan = Math.atan;
+    var atan2$1 = Math.atan2;
+    var cos$1 = Math.cos;
+    var ceil = Math.ceil;
+    var exp = Math.exp;
+    var hypot = Math.hypot;
+    var log$1 = Math.log;
+    var pow$1 = Math.pow;
+    var sin$1 = Math.sin;
+    var sign$1 = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; };
+    var sqrt$2 = Math.sqrt;
+    var tan = Math.tan;
+    
+    function acos$1(x) {
+      return x > 1 ? 0 : x < -1 ? pi$1 : Math.acos(x);
+    }
+    
+    function asin$1(x) {
+      return x > 1 ? halfPi$1 : x < -1 ? -halfPi$1 : Math.asin(x);
+    }
+    
+    function haversin(x) {
+      return (x = sin$1(x / 2)) * x;
+    }
+    
+    function noop$1() {}
+    
+    function streamGeometry(geometry, stream) {
+      if (geometry && streamGeometryType.hasOwnProperty(geometry.type)) {
+        streamGeometryType[geometry.type](geometry, stream);
+      }
+    }
+    
+    var streamObjectType = {
+      Feature: function(object, stream) {
+        streamGeometry(object.geometry, stream);
+      },
+      FeatureCollection: function(object, stream) {
+        var features = object.features, i = -1, n = features.length;
+        while (++i < n) streamGeometry(features[i].geometry, stream);
+      }
+    };
+    
+    var streamGeometryType = {
+      Sphere: function(object, stream) {
+        stream.sphere();
+      },
+      Point: function(object, stream) {
+        object = object.coordinates;
+        stream.point(object[0], object[1], object[2]);
+      },
+      MultiPoint: function(object, stream) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) object = coordinates[i], stream.point(object[0], object[1], object[2]);
+      },
+      LineString: function(object, stream) {
+        streamLine(object.coordinates, stream, 0);
+      },
+      MultiLineString: function(object, stream) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) streamLine(coordinates[i], stream, 0);
+      },
+      Polygon: function(object, stream) {
+        streamPolygon(object.coordinates, stream);
+      },
+      MultiPolygon: function(object, stream) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) streamPolygon(coordinates[i], stream);
+      },
+      GeometryCollection: function(object, stream) {
+        var geometries = object.geometries, i = -1, n = geometries.length;
+        while (++i < n) streamGeometry(geometries[i], stream);
+      }
+    };
+    
+    function streamLine(coordinates, stream, closed) {
+      var i = -1, n = coordinates.length - closed, coordinate;
+      stream.lineStart();
+      while (++i < n) coordinate = coordinates[i], stream.point(coordinate[0], coordinate[1], coordinate[2]);
+      stream.lineEnd();
+    }
+    
+    function streamPolygon(coordinates, stream) {
+      var i = -1, n = coordinates.length;
+      stream.polygonStart();
+      while (++i < n) streamLine(coordinates[i], stream, 1);
+      stream.polygonEnd();
+    }
+    
+    function geoStream(object, stream) {
+      if (object && streamObjectType.hasOwnProperty(object.type)) {
+        streamObjectType[object.type](object, stream);
+      } else {
+        streamGeometry(object, stream);
+      }
+    }
+    
+    var areaRingSum$1 = new Adder();
+    
+    // hello?
+    
+    var areaSum$1 = new Adder(),
+        lambda00$2,
+        phi00$2,
+        lambda0$2,
+        cosPhi0$1,
+        sinPhi0$1;
+    
+    var areaStream$1 = {
+      point: noop$1,
+      lineStart: noop$1,
+      lineEnd: noop$1,
+      polygonStart: function() {
+        areaRingSum$1 = new Adder();
+        areaStream$1.lineStart = areaRingStart$1;
+        areaStream$1.lineEnd = areaRingEnd$1;
+      },
+      polygonEnd: function() {
+        var areaRing = +areaRingSum$1;
+        areaSum$1.add(areaRing < 0 ? tau$1 + areaRing : areaRing);
+        this.lineStart = this.lineEnd = this.point = noop$1;
+      },
+      sphere: function() {
+        areaSum$1.add(tau$1);
+      }
+    };
+    
+    function areaRingStart$1() {
+      areaStream$1.point = areaPointFirst$1;
+    }
+    
+    function areaRingEnd$1() {
+      areaPoint$1(lambda00$2, phi00$2);
+    }
+    
+    function areaPointFirst$1(lambda, phi) {
+      areaStream$1.point = areaPoint$1;
+      lambda00$2 = lambda, phi00$2 = phi;
+      lambda *= radians, phi *= radians;
+      lambda0$2 = lambda, cosPhi0$1 = cos$1(phi = phi / 2 + quarterPi), sinPhi0$1 = sin$1(phi);
+    }
+    
+    function areaPoint$1(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      phi = phi / 2 + quarterPi; // half the angular distance from south pole
+    
+      // Spherical excess E for a spherical triangle with vertices: south pole,
+      // previous point, current point.  Uses a formula derived from Cagnoli’s
+      // theorem.  See Todhunter, Spherical Trig. (1871), Sec. 103, Eq. (2).
+      var dLambda = lambda - lambda0$2,
+          sdLambda = dLambda >= 0 ? 1 : -1,
+          adLambda = sdLambda * dLambda,
+          cosPhi = cos$1(phi),
+          sinPhi = sin$1(phi),
+          k = sinPhi0$1 * sinPhi,
+          u = cosPhi0$1 * cosPhi + k * cos$1(adLambda),
+          v = k * sdLambda * sin$1(adLambda);
+      areaRingSum$1.add(atan2$1(v, u));
+    
+      // Advance the previous points.
+      lambda0$2 = lambda, cosPhi0$1 = cosPhi, sinPhi0$1 = sinPhi;
+    }
+    
+    function area$2(object) {
+      areaSum$1 = new Adder();
+      geoStream(object, areaStream$1);
+      return areaSum$1 * 2;
+    }
+    
+    function spherical(cartesian) {
+      return [atan2$1(cartesian[1], cartesian[0]), asin$1(cartesian[2])];
+    }
+    
+    function cartesian(spherical) {
+      var lambda = spherical[0], phi = spherical[1], cosPhi = cos$1(phi);
+      return [cosPhi * cos$1(lambda), cosPhi * sin$1(lambda), sin$1(phi)];
+    }
+    
+    function cartesianDot(a, b) {
+      return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+    }
+    
+    function cartesianCross(a, b) {
+      return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
+    }
+    
+    // TODO return a
+    function cartesianAddInPlace(a, b) {
+      a[0] += b[0], a[1] += b[1], a[2] += b[2];
+    }
+    
+    function cartesianScale(vector, k) {
+      return [vector[0] * k, vector[1] * k, vector[2] * k];
+    }
+    
+    // TODO return d
+    function cartesianNormalizeInPlace(d) {
+      var l = sqrt$2(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
+      d[0] /= l, d[1] /= l, d[2] /= l;
+    }
+    
+    var lambda0$1, phi0, lambda1, phi1, // bounds
+        lambda2, // previous lambda-coordinate
+        lambda00$1, phi00$1, // first point
+        p0, // previous 3D point
+        deltaSum,
+        ranges,
+        range;
+    
+    var boundsStream$1 = {
+      point: boundsPoint$1,
+      lineStart: boundsLineStart,
+      lineEnd: boundsLineEnd,
+      polygonStart: function() {
+        boundsStream$1.point = boundsRingPoint;
+        boundsStream$1.lineStart = boundsRingStart;
+        boundsStream$1.lineEnd = boundsRingEnd;
+        deltaSum = new Adder();
+        areaStream$1.polygonStart();
+      },
+      polygonEnd: function() {
+        areaStream$1.polygonEnd();
+        boundsStream$1.point = boundsPoint$1;
+        boundsStream$1.lineStart = boundsLineStart;
+        boundsStream$1.lineEnd = boundsLineEnd;
+        if (areaRingSum$1 < 0) lambda0$1 = -(lambda1 = 180), phi0 = -(phi1 = 90);
+        else if (deltaSum > epsilon$1) phi1 = 90;
+        else if (deltaSum < -epsilon$1) phi0 = -90;
+        range[0] = lambda0$1, range[1] = lambda1;
+      },
+      sphere: function() {
+        lambda0$1 = -(lambda1 = 180), phi0 = -(phi1 = 90);
+      }
+    };
+    
+    function boundsPoint$1(lambda, phi) {
+      ranges.push(range = [lambda0$1 = lambda, lambda1 = lambda]);
+      if (phi < phi0) phi0 = phi;
+      if (phi > phi1) phi1 = phi;
+    }
+    
+    function linePoint(lambda, phi) {
+      var p = cartesian([lambda * radians, phi * radians]);
+      if (p0) {
+        var normal = cartesianCross(p0, p),
+            equatorial = [normal[1], -normal[0], 0],
+            inflection = cartesianCross(equatorial, normal);
+        cartesianNormalizeInPlace(inflection);
+        inflection = spherical(inflection);
+        var delta = lambda - lambda2,
+            sign = delta > 0 ? 1 : -1,
+            lambdai = inflection[0] * degrees * sign,
+            phii,
+            antimeridian = abs$1(delta) > 180;
+        if (antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {
+          phii = inflection[1] * degrees;
+          if (phii > phi1) phi1 = phii;
+        } else if (lambdai = (lambdai + 360) % 360 - 180, antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {
+          phii = -inflection[1] * degrees;
+          if (phii < phi0) phi0 = phii;
+        } else {
+          if (phi < phi0) phi0 = phi;
+          if (phi > phi1) phi1 = phi;
+        }
+        if (antimeridian) {
+          if (lambda < lambda2) {
+            if (angle(lambda0$1, lambda) > angle(lambda0$1, lambda1)) lambda1 = lambda;
+          } else {
+            if (angle(lambda, lambda1) > angle(lambda0$1, lambda1)) lambda0$1 = lambda;
+          }
+        } else {
+          if (lambda1 >= lambda0$1) {
+            if (lambda < lambda0$1) lambda0$1 = lambda;
+            if (lambda > lambda1) lambda1 = lambda;
+          } else {
+            if (lambda > lambda2) {
+              if (angle(lambda0$1, lambda) > angle(lambda0$1, lambda1)) lambda1 = lambda;
+            } else {
+              if (angle(lambda, lambda1) > angle(lambda0$1, lambda1)) lambda0$1 = lambda;
+            }
+          }
+        }
+      } else {
+        ranges.push(range = [lambda0$1 = lambda, lambda1 = lambda]);
+      }
+      if (phi < phi0) phi0 = phi;
+      if (phi > phi1) phi1 = phi;
+      p0 = p, lambda2 = lambda;
+    }
+    
+    function boundsLineStart() {
+      boundsStream$1.point = linePoint;
+    }
+    
+    function boundsLineEnd() {
+      range[0] = lambda0$1, range[1] = lambda1;
+      boundsStream$1.point = boundsPoint$1;
+      p0 = null;
+    }
+    
+    function boundsRingPoint(lambda, phi) {
+      if (p0) {
+        var delta = lambda - lambda2;
+        deltaSum.add(abs$1(delta) > 180 ? delta + (delta > 0 ? 360 : -360) : delta);
+      } else {
+        lambda00$1 = lambda, phi00$1 = phi;
+      }
+      areaStream$1.point(lambda, phi);
+      linePoint(lambda, phi);
+    }
+    
+    function boundsRingStart() {
+      areaStream$1.lineStart();
+    }
+    
+    function boundsRingEnd() {
+      boundsRingPoint(lambda00$1, phi00$1);
+      areaStream$1.lineEnd();
+      if (abs$1(deltaSum) > epsilon$1) lambda0$1 = -(lambda1 = 180);
+      range[0] = lambda0$1, range[1] = lambda1;
+      p0 = null;
+    }
+    
+    // Finds the left-right distance between two longitudes.
+    // This is almost the same as (lambda1 - lambda0 + 360°) % 360°, except that we want
+    // the distance between ±180° to be 360°.
+    function angle(lambda0, lambda1) {
+      return (lambda1 -= lambda0) < 0 ? lambda1 + 360 : lambda1;
+    }
+    
+    function rangeCompare(a, b) {
+      return a[0] - b[0];
+    }
+    
+    function rangeContains(range, x) {
+      return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;
+    }
+    
+    function bounds(feature) {
+      var i, n, a, b, merged, deltaMax, delta;
+    
+      phi1 = lambda1 = -(lambda0$1 = phi0 = Infinity);
+      ranges = [];
+      geoStream(feature, boundsStream$1);
+    
+      // First, sort ranges by their minimum longitudes.
+      if (n = ranges.length) {
+        ranges.sort(rangeCompare);
+    
+        // Then, merge any ranges that overlap.
+        for (i = 1, a = ranges[0], merged = [a]; i < n; ++i) {
+          b = ranges[i];
+          if (rangeContains(a, b[0]) || rangeContains(a, b[1])) {
+            if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];
+            if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];
+          } else {
+            merged.push(a = b);
+          }
+        }
+    
+        // Finally, find the largest gap between the merged ranges.
+        // The final bounding box will be the inverse of this gap.
+        for (deltaMax = -Infinity, n = merged.length - 1, i = 0, a = merged[n]; i <= n; a = b, ++i) {
+          b = merged[i];
+          if ((delta = angle(a[1], b[0])) > deltaMax) deltaMax = delta, lambda0$1 = b[0], lambda1 = a[1];
+        }
+      }
+    
+      ranges = range = null;
+    
+      return lambda0$1 === Infinity || phi0 === Infinity
+          ? [[NaN, NaN], [NaN, NaN]]
+          : [[lambda0$1, phi0], [lambda1, phi1]];
+    }
+    
+    var W0, W1,
+        X0$1, Y0$1, Z0$1,
+        X1$1, Y1$1, Z1$1,
+        X2$1, Y2$1, Z2$1,
+        lambda00, phi00, // first point
+        x0$4, y0$4, z0; // previous point
+    
+    var centroidStream$1 = {
+      sphere: noop$1,
+      point: centroidPoint$1,
+      lineStart: centroidLineStart$1,
+      lineEnd: centroidLineEnd$1,
+      polygonStart: function() {
+        centroidStream$1.lineStart = centroidRingStart$1;
+        centroidStream$1.lineEnd = centroidRingEnd$1;
+      },
+      polygonEnd: function() {
+        centroidStream$1.lineStart = centroidLineStart$1;
+        centroidStream$1.lineEnd = centroidLineEnd$1;
+      }
+    };
+    
+    // Arithmetic mean of Cartesian vectors.
+    function centroidPoint$1(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      var cosPhi = cos$1(phi);
+      centroidPointCartesian(cosPhi * cos$1(lambda), cosPhi * sin$1(lambda), sin$1(phi));
+    }
+    
+    function centroidPointCartesian(x, y, z) {
+      ++W0;
+      X0$1 += (x - X0$1) / W0;
+      Y0$1 += (y - Y0$1) / W0;
+      Z0$1 += (z - Z0$1) / W0;
+    }
+    
+    function centroidLineStart$1() {
+      centroidStream$1.point = centroidLinePointFirst;
+    }
+    
+    function centroidLinePointFirst(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      var cosPhi = cos$1(phi);
+      x0$4 = cosPhi * cos$1(lambda);
+      y0$4 = cosPhi * sin$1(lambda);
+      z0 = sin$1(phi);
+      centroidStream$1.point = centroidLinePoint;
+      centroidPointCartesian(x0$4, y0$4, z0);
+    }
+    
+    function centroidLinePoint(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      var cosPhi = cos$1(phi),
+          x = cosPhi * cos$1(lambda),
+          y = cosPhi * sin$1(lambda),
+          z = sin$1(phi),
+          w = atan2$1(sqrt$2((w = y0$4 * z - z0 * y) * w + (w = z0 * x - x0$4 * z) * w + (w = x0$4 * y - y0$4 * x) * w), x0$4 * x + y0$4 * y + z0 * z);
+      W1 += w;
+      X1$1 += w * (x0$4 + (x0$4 = x));
+      Y1$1 += w * (y0$4 + (y0$4 = y));
+      Z1$1 += w * (z0 + (z0 = z));
+      centroidPointCartesian(x0$4, y0$4, z0);
+    }
+    
+    function centroidLineEnd$1() {
+      centroidStream$1.point = centroidPoint$1;
+    }
+    
+    // See J. E. Brock, The Inertia Tensor for a Spherical Triangle,
+    // J. Applied Mechanics 42, 239 (1975).
+    function centroidRingStart$1() {
+      centroidStream$1.point = centroidRingPointFirst;
+    }
+    
+    function centroidRingEnd$1() {
+      centroidRingPoint(lambda00, phi00);
+      centroidStream$1.point = centroidPoint$1;
+    }
+    
+    function centroidRingPointFirst(lambda, phi) {
+      lambda00 = lambda, phi00 = phi;
+      lambda *= radians, phi *= radians;
+      centroidStream$1.point = centroidRingPoint;
+      var cosPhi = cos$1(phi);
+      x0$4 = cosPhi * cos$1(lambda);
+      y0$4 = cosPhi * sin$1(lambda);
+      z0 = sin$1(phi);
+      centroidPointCartesian(x0$4, y0$4, z0);
+    }
+    
+    function centroidRingPoint(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      var cosPhi = cos$1(phi),
+          x = cosPhi * cos$1(lambda),
+          y = cosPhi * sin$1(lambda),
+          z = sin$1(phi),
+          cx = y0$4 * z - z0 * y,
+          cy = z0 * x - x0$4 * z,
+          cz = x0$4 * y - y0$4 * x,
+          m = hypot(cx, cy, cz),
+          w = asin$1(m), // line weight = angle
+          v = m && -w / m; // area weight multiplier
+      X2$1.add(v * cx);
+      Y2$1.add(v * cy);
+      Z2$1.add(v * cz);
+      W1 += w;
+      X1$1 += w * (x0$4 + (x0$4 = x));
+      Y1$1 += w * (y0$4 + (y0$4 = y));
+      Z1$1 += w * (z0 + (z0 = z));
+      centroidPointCartesian(x0$4, y0$4, z0);
+    }
+    
+    function centroid$1(object) {
+      W0 = W1 =
+      X0$1 = Y0$1 = Z0$1 =
+      X1$1 = Y1$1 = Z1$1 = 0;
+      X2$1 = new Adder();
+      Y2$1 = new Adder();
+      Z2$1 = new Adder();
+      geoStream(object, centroidStream$1);
+    
+      var x = +X2$1,
+          y = +Y2$1,
+          z = +Z2$1,
+          m = hypot(x, y, z);
+    
+      // If the area-weighted ccentroid is undefined, fall back to length-weighted ccentroid.
+      if (m < epsilon2) {
+        x = X1$1, y = Y1$1, z = Z1$1;
+        // If the feature has zero length, fall back to arithmetic mean of point vectors.
+        if (W1 < epsilon$1) x = X0$1, y = Y0$1, z = Z0$1;
+        m = hypot(x, y, z);
+        // If the feature still has an undefined ccentroid, then return.
+        if (m < epsilon2) return [NaN, NaN];
+      }
+    
+      return [atan2$1(y, x) * degrees, asin$1(z / m) * degrees];
+    }
+    
+    function constant$3(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function compose(a, b) {
+    
+      function compose(x, y) {
+        return x = a(x, y), b(x[0], x[1]);
+      }
+    
+      if (a.invert && b.invert) compose.invert = function(x, y) {
+        return x = b.invert(x, y), x && a.invert(x[0], x[1]);
+      };
+    
+      return compose;
+    }
+    
+    function rotationIdentity(lambda, phi) {
+      return [abs$1(lambda) > pi$1 ? lambda + Math.round(-lambda / tau$1) * tau$1 : lambda, phi];
+    }
+    
+    rotationIdentity.invert = rotationIdentity;
+    
+    function rotateRadians(deltaLambda, deltaPhi, deltaGamma) {
+      return (deltaLambda %= tau$1) ? (deltaPhi || deltaGamma ? compose(rotationLambda(deltaLambda), rotationPhiGamma(deltaPhi, deltaGamma))
+        : rotationLambda(deltaLambda))
+        : (deltaPhi || deltaGamma ? rotationPhiGamma(deltaPhi, deltaGamma)
+        : rotationIdentity);
+    }
+    
+    function forwardRotationLambda(deltaLambda) {
+      return function(lambda, phi) {
+        return lambda += deltaLambda, [lambda > pi$1 ? lambda - tau$1 : lambda < -pi$1 ? lambda + tau$1 : lambda, phi];
+      };
+    }
+    
+    function rotationLambda(deltaLambda) {
+      var rotation = forwardRotationLambda(deltaLambda);
+      rotation.invert = forwardRotationLambda(-deltaLambda);
+      return rotation;
+    }
+    
+    function rotationPhiGamma(deltaPhi, deltaGamma) {
+      var cosDeltaPhi = cos$1(deltaPhi),
+          sinDeltaPhi = sin$1(deltaPhi),
+          cosDeltaGamma = cos$1(deltaGamma),
+          sinDeltaGamma = sin$1(deltaGamma);
+    
+      function rotation(lambda, phi) {
+        var cosPhi = cos$1(phi),
+            x = cos$1(lambda) * cosPhi,
+            y = sin$1(lambda) * cosPhi,
+            z = sin$1(phi),
+            k = z * cosDeltaPhi + x * sinDeltaPhi;
+        return [
+          atan2$1(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi),
+          asin$1(k * cosDeltaGamma + y * sinDeltaGamma)
+        ];
+      }
+    
+      rotation.invert = function(lambda, phi) {
+        var cosPhi = cos$1(phi),
+            x = cos$1(lambda) * cosPhi,
+            y = sin$1(lambda) * cosPhi,
+            z = sin$1(phi),
+            k = z * cosDeltaGamma - y * sinDeltaGamma;
+        return [
+          atan2$1(y * cosDeltaGamma + z * sinDeltaGamma, x * cosDeltaPhi + k * sinDeltaPhi),
+          asin$1(k * cosDeltaPhi - x * sinDeltaPhi)
+        ];
+      };
+    
+      return rotation;
+    }
+    
+    function rotation(rotate) {
+      rotate = rotateRadians(rotate[0] * radians, rotate[1] * radians, rotate.length > 2 ? rotate[2] * radians : 0);
+    
+      function forward(coordinates) {
+        coordinates = rotate(coordinates[0] * radians, coordinates[1] * radians);
+        return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates;
+      }
+    
+      forward.invert = function(coordinates) {
+        coordinates = rotate.invert(coordinates[0] * radians, coordinates[1] * radians);
+        return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates;
+      };
+    
+      return forward;
+    }
+    
+    // Generates a circle centered at [0°, 0°], with a given radius and precision.
+    function circleStream(stream, radius, delta, direction, t0, t1) {
+      if (!delta) return;
+      var cosRadius = cos$1(radius),
+          sinRadius = sin$1(radius),
+          step = direction * delta;
+      if (t0 == null) {
+        t0 = radius + direction * tau$1;
+        t1 = radius - step / 2;
+      } else {
+        t0 = circleRadius(cosRadius, t0);
+        t1 = circleRadius(cosRadius, t1);
+        if (direction > 0 ? t0 < t1 : t0 > t1) t0 += direction * tau$1;
+      }
+      for (var point, t = t0; direction > 0 ? t > t1 : t < t1; t -= step) {
+        point = spherical([cosRadius, -sinRadius * cos$1(t), -sinRadius * sin$1(t)]);
+        stream.point(point[0], point[1]);
+      }
+    }
+    
+    // Returns the signed angle of a cartesian point relative to [cosRadius, 0, 0].
+    function circleRadius(cosRadius, point) {
+      point = cartesian(point), point[0] -= cosRadius;
+      cartesianNormalizeInPlace(point);
+      var radius = acos$1(-point[1]);
+      return ((-point[2] < 0 ? -radius : radius) + tau$1 - epsilon$1) % tau$1;
+    }
+    
+    function circle$2() {
+      var center = constant$3([0, 0]),
+          radius = constant$3(90),
+          precision = constant$3(6),
+          ring,
+          rotate,
+          stream = {point: point};
+    
+      function point(x, y) {
+        ring.push(x = rotate(x, y));
+        x[0] *= degrees, x[1] *= degrees;
+      }
+    
+      function circle() {
+        var c = center.apply(this, arguments),
+            r = radius.apply(this, arguments) * radians,
+            p = precision.apply(this, arguments) * radians;
+        ring = [];
+        rotate = rotateRadians(-c[0] * radians, -c[1] * radians, 0).invert;
+        circleStream(stream, r, p, 1);
+        c = {type: "Polygon", coordinates: [ring]};
+        ring = rotate = null;
+        return c;
+      }
+    
+      circle.center = function(_) {
+        return arguments.length ? (center = typeof _ === "function" ? _ : constant$3([+_[0], +_[1]]), circle) : center;
+      };
+    
+      circle.radius = function(_) {
+        return arguments.length ? (radius = typeof _ === "function" ? _ : constant$3(+_), circle) : radius;
+      };
+    
+      circle.precision = function(_) {
+        return arguments.length ? (precision = typeof _ === "function" ? _ : constant$3(+_), circle) : precision;
+      };
+    
+      return circle;
+    }
+    
+    function clipBuffer() {
+      var lines = [],
+          line;
+      return {
+        point: function(x, y, m) {
+          line.push([x, y, m]);
+        },
+        lineStart: function() {
+          lines.push(line = []);
+        },
+        lineEnd: noop$1,
+        rejoin: function() {
+          if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));
+        },
+        result: function() {
+          var result = lines;
+          lines = [];
+          line = null;
+          return result;
+        }
+      };
+    }
+    
+    function pointEqual(a, b) {
+      return abs$1(a[0] - b[0]) < epsilon$1 && abs$1(a[1] - b[1]) < epsilon$1;
+    }
+    
+    function Intersection(point, points, other, entry) {
+      this.x = point;
+      this.z = points;
+      this.o = other; // another intersection
+      this.e = entry; // is an entry?
+      this.v = false; // visited
+      this.n = this.p = null; // next & previous
+    }
+    
+    // A generalized polygon clipping algorithm: given a polygon that has been cut
+    // into its visible line segments, and rejoins the segments by interpolating
+    // along the clip edge.
+    function clipRejoin(segments, compareIntersection, startInside, interpolate, stream) {
+      var subject = [],
+          clip = [],
+          i,
+          n;
+    
+      segments.forEach(function(segment) {
+        if ((n = segment.length - 1) <= 0) return;
+        var n, p0 = segment[0], p1 = segment[n], x;
+    
+        if (pointEqual(p0, p1)) {
+          if (!p0[2] && !p1[2]) {
+            stream.lineStart();
+            for (i = 0; i < n; ++i) stream.point((p0 = segment[i])[0], p0[1]);
+            stream.lineEnd();
+            return;
+          }
+          // handle degenerate cases by moving the point
+          p1[0] += 2 * epsilon$1;
+        }
+    
+        subject.push(x = new Intersection(p0, segment, null, true));
+        clip.push(x.o = new Intersection(p0, null, x, false));
+        subject.push(x = new Intersection(p1, segment, null, false));
+        clip.push(x.o = new Intersection(p1, null, x, true));
+      });
+    
+      if (!subject.length) return;
+    
+      clip.sort(compareIntersection);
+      link$1(subject);
+      link$1(clip);
+    
+      for (i = 0, n = clip.length; i < n; ++i) {
+        clip[i].e = startInside = !startInside;
+      }
+    
+      var start = subject[0],
+          points,
+          point;
+    
+      while (1) {
+        // Find first unvisited intersection.
+        var current = start,
+            isSubject = true;
+        while (current.v) if ((current = current.n) === start) return;
+        points = current.z;
+        stream.lineStart();
+        do {
+          current.v = current.o.v = true;
+          if (current.e) {
+            if (isSubject) {
+              for (i = 0, n = points.length; i < n; ++i) stream.point((point = points[i])[0], point[1]);
+            } else {
+              interpolate(current.x, current.n.x, 1, stream);
+            }
+            current = current.n;
+          } else {
+            if (isSubject) {
+              points = current.p.z;
+              for (i = points.length - 1; i >= 0; --i) stream.point((point = points[i])[0], point[1]);
+            } else {
+              interpolate(current.x, current.p.x, -1, stream);
+            }
+            current = current.p;
+          }
+          current = current.o;
+          points = current.z;
+          isSubject = !isSubject;
+        } while (!current.v);
+        stream.lineEnd();
+      }
+    }
+    
+    function link$1(array) {
+      if (!(n = array.length)) return;
+      var n,
+          i = 0,
+          a = array[0],
+          b;
+      while (++i < n) {
+        a.n = b = array[i];
+        b.p = a;
+        a = b;
+      }
+      a.n = b = array[0];
+      b.p = a;
+    }
+    
+    function longitude(point) {
+      if (abs$1(point[0]) <= pi$1)
+        return point[0];
+      else
+        return sign$1(point[0]) * ((abs$1(point[0]) + pi$1) % tau$1 - pi$1);
+    }
+    
+    function polygonContains(polygon, point) {
+      var lambda = longitude(point),
+          phi = point[1],
+          sinPhi = sin$1(phi),
+          normal = [sin$1(lambda), -cos$1(lambda), 0],
+          angle = 0,
+          winding = 0;
+    
+      var sum = new Adder();
+    
+      if (sinPhi === 1) phi = halfPi$1 + epsilon$1;
+      else if (sinPhi === -1) phi = -halfPi$1 - epsilon$1;
+    
+      for (var i = 0, n = polygon.length; i < n; ++i) {
+        if (!(m = (ring = polygon[i]).length)) continue;
+        var ring,
+            m,
+            point0 = ring[m - 1],
+            lambda0 = longitude(point0),
+            phi0 = point0[1] / 2 + quarterPi,
+            sinPhi0 = sin$1(phi0),
+            cosPhi0 = cos$1(phi0);
+    
+        for (var j = 0; j < m; ++j, lambda0 = lambda1, sinPhi0 = sinPhi1, cosPhi0 = cosPhi1, point0 = point1) {
+          var point1 = ring[j],
+              lambda1 = longitude(point1),
+              phi1 = point1[1] / 2 + quarterPi,
+              sinPhi1 = sin$1(phi1),
+              cosPhi1 = cos$1(phi1),
+              delta = lambda1 - lambda0,
+              sign = delta >= 0 ? 1 : -1,
+              absDelta = sign * delta,
+              antimeridian = absDelta > pi$1,
+              k = sinPhi0 * sinPhi1;
+    
+          sum.add(atan2$1(k * sign * sin$1(absDelta), cosPhi0 * cosPhi1 + k * cos$1(absDelta)));
+          angle += antimeridian ? delta + sign * tau$1 : delta;
+    
+          // Are the longitudes either side of the point’s meridian (lambda),
+          // and are the latitudes smaller than the parallel (phi)?
+          if (antimeridian ^ lambda0 >= lambda ^ lambda1 >= lambda) {
+            var arc = cartesianCross(cartesian(point0), cartesian(point1));
+            cartesianNormalizeInPlace(arc);
+            var intersection = cartesianCross(normal, arc);
+            cartesianNormalizeInPlace(intersection);
+            var phiArc = (antimeridian ^ delta >= 0 ? -1 : 1) * asin$1(intersection[2]);
+            if (phi > phiArc || phi === phiArc && (arc[0] || arc[1])) {
+              winding += antimeridian ^ delta >= 0 ? 1 : -1;
+            }
+          }
+        }
+      }
+    
+      // First, determine whether the South pole is inside or outside:
+      //
+      // It is inside if:
+      // * the polygon winds around it in a clockwise direction.
+      // * the polygon does not (cumulatively) wind around it, but has a negative
+      //   (counter-clockwise) area.
+      //
+      // Second, count the (signed) number of times a segment crosses a lambda
+      // from the point to the South pole.  If it is zero, then the point is the
+      // same side as the South pole.
+    
+      return (angle < -epsilon$1 || angle < epsilon$1 && sum < -epsilon2) ^ (winding & 1);
+    }
+    
+    function clip(pointVisible, clipLine, interpolate, start) {
+      return function(sink) {
+        var line = clipLine(sink),
+            ringBuffer = clipBuffer(),
+            ringSink = clipLine(ringBuffer),
+            polygonStarted = false,
+            polygon,
+            segments,
+            ring;
+    
+        var clip = {
+          point: point,
+          lineStart: lineStart,
+          lineEnd: lineEnd,
+          polygonStart: function() {
+            clip.point = pointRing;
+            clip.lineStart = ringStart;
+            clip.lineEnd = ringEnd;
+            segments = [];
+            polygon = [];
+          },
+          polygonEnd: function() {
+            clip.point = point;
+            clip.lineStart = lineStart;
+            clip.lineEnd = lineEnd;
+            segments = merge(segments);
+            var startInside = polygonContains(polygon, start);
+            if (segments.length) {
+              if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+              clipRejoin(segments, compareIntersection, startInside, interpolate, sink);
+            } else if (startInside) {
+              if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+              sink.lineStart();
+              interpolate(null, null, 1, sink);
+              sink.lineEnd();
+            }
+            if (polygonStarted) sink.polygonEnd(), polygonStarted = false;
+            segments = polygon = null;
+          },
+          sphere: function() {
+            sink.polygonStart();
+            sink.lineStart();
+            interpolate(null, null, 1, sink);
+            sink.lineEnd();
+            sink.polygonEnd();
+          }
+        };
+    
+        function point(lambda, phi) {
+          if (pointVisible(lambda, phi)) sink.point(lambda, phi);
+        }
+    
+        function pointLine(lambda, phi) {
+          line.point(lambda, phi);
+        }
+    
+        function lineStart() {
+          clip.point = pointLine;
+          line.lineStart();
+        }
+    
+        function lineEnd() {
+          clip.point = point;
+          line.lineEnd();
+        }
+    
+        function pointRing(lambda, phi) {
+          ring.push([lambda, phi]);
+          ringSink.point(lambda, phi);
+        }
+    
+        function ringStart() {
+          ringSink.lineStart();
+          ring = [];
+        }
+    
+        function ringEnd() {
+          pointRing(ring[0][0], ring[0][1]);
+          ringSink.lineEnd();
+    
+          var clean = ringSink.clean(),
+              ringSegments = ringBuffer.result(),
+              i, n = ringSegments.length, m,
+              segment,
+              point;
+    
+          ring.pop();
+          polygon.push(ring);
+          ring = null;
+    
+          if (!n) return;
+    
+          // No intersections.
+          if (clean & 1) {
+            segment = ringSegments[0];
+            if ((m = segment.length - 1) > 0) {
+              if (!polygonStarted) sink.polygonStart(), polygonStarted = true;
+              sink.lineStart();
+              for (i = 0; i < m; ++i) sink.point((point = segment[i])[0], point[1]);
+              sink.lineEnd();
+            }
+            return;
+          }
+    
+          // Rejoin connected segments.
+          // TODO reuse ringBuffer.rejoin()?
+          if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));
+    
+          segments.push(ringSegments.filter(validSegment));
+        }
+    
+        return clip;
+      };
+    }
+    
+    function validSegment(segment) {
+      return segment.length > 1;
+    }
+    
+    // Intersections are sorted along the clip edge. For both antimeridian cutting
+    // and circle clipping, the same comparison is used.
+    function compareIntersection(a, b) {
+      return ((a = a.x)[0] < 0 ? a[1] - halfPi$1 - epsilon$1 : halfPi$1 - a[1])
+           - ((b = b.x)[0] < 0 ? b[1] - halfPi$1 - epsilon$1 : halfPi$1 - b[1]);
+    }
+    
+    var clipAntimeridian = clip(
+      function() { return true; },
+      clipAntimeridianLine,
+      clipAntimeridianInterpolate,
+      [-pi$1, -halfPi$1]
+    );
+    
+    // Takes a line and cuts into visible segments. Return values: 0 - there were
+    // intersections or the line was empty; 1 - no intersections; 2 - there were
+    // intersections, and the first and last segments should be rejoined.
+    function clipAntimeridianLine(stream) {
+      var lambda0 = NaN,
+          phi0 = NaN,
+          sign0 = NaN,
+          clean; // no intersections
+    
+      return {
+        lineStart: function() {
+          stream.lineStart();
+          clean = 1;
+        },
+        point: function(lambda1, phi1) {
+          var sign1 = lambda1 > 0 ? pi$1 : -pi$1,
+              delta = abs$1(lambda1 - lambda0);
+          if (abs$1(delta - pi$1) < epsilon$1) { // line crosses a pole
+            stream.point(lambda0, phi0 = (phi0 + phi1) / 2 > 0 ? halfPi$1 : -halfPi$1);
+            stream.point(sign0, phi0);
+            stream.lineEnd();
+            stream.lineStart();
+            stream.point(sign1, phi0);
+            stream.point(lambda1, phi0);
+            clean = 0;
+          } else if (sign0 !== sign1 && delta >= pi$1) { // line crosses antimeridian
+            if (abs$1(lambda0 - sign0) < epsilon$1) lambda0 -= sign0 * epsilon$1; // handle degeneracies
+            if (abs$1(lambda1 - sign1) < epsilon$1) lambda1 -= sign1 * epsilon$1;
+            phi0 = clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1);
+            stream.point(sign0, phi0);
+            stream.lineEnd();
+            stream.lineStart();
+            stream.point(sign1, phi0);
+            clean = 0;
+          }
+          stream.point(lambda0 = lambda1, phi0 = phi1);
+          sign0 = sign1;
+        },
+        lineEnd: function() {
+          stream.lineEnd();
+          lambda0 = phi0 = NaN;
+        },
+        clean: function() {
+          return 2 - clean; // if intersections, rejoin first and last segments
+        }
+      };
+    }
+    
+    function clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1) {
+      var cosPhi0,
+          cosPhi1,
+          sinLambda0Lambda1 = sin$1(lambda0 - lambda1);
+      return abs$1(sinLambda0Lambda1) > epsilon$1
+          ? atan((sin$1(phi0) * (cosPhi1 = cos$1(phi1)) * sin$1(lambda1)
+              - sin$1(phi1) * (cosPhi0 = cos$1(phi0)) * sin$1(lambda0))
+              / (cosPhi0 * cosPhi1 * sinLambda0Lambda1))
+          : (phi0 + phi1) / 2;
+    }
+    
+    function clipAntimeridianInterpolate(from, to, direction, stream) {
+      var phi;
+      if (from == null) {
+        phi = direction * halfPi$1;
+        stream.point(-pi$1, phi);
+        stream.point(0, phi);
+        stream.point(pi$1, phi);
+        stream.point(pi$1, 0);
+        stream.point(pi$1, -phi);
+        stream.point(0, -phi);
+        stream.point(-pi$1, -phi);
+        stream.point(-pi$1, 0);
+        stream.point(-pi$1, phi);
+      } else if (abs$1(from[0] - to[0]) > epsilon$1) {
+        var lambda = from[0] < to[0] ? pi$1 : -pi$1;
+        phi = direction * lambda / 2;
+        stream.point(-lambda, phi);
+        stream.point(0, phi);
+        stream.point(lambda, phi);
+      } else {
+        stream.point(to[0], to[1]);
+      }
+    }
+    
+    function clipCircle(radius) {
+      var cr = cos$1(radius),
+          delta = 6 * radians,
+          smallRadius = cr > 0,
+          notHemisphere = abs$1(cr) > epsilon$1; // TODO optimise for this common case
+    
+      function interpolate(from, to, direction, stream) {
+        circleStream(stream, radius, delta, direction, from, to);
+      }
+    
+      function visible(lambda, phi) {
+        return cos$1(lambda) * cos$1(phi) > cr;
+      }
+    
+      // Takes a line and cuts into visible segments. Return values used for polygon
+      // clipping: 0 - there were intersections or the line was empty; 1 - no
+      // intersections 2 - there were intersections, and the first and last segments
+      // should be rejoined.
+      function clipLine(stream) {
+        var point0, // previous point
+            c0, // code for previous point
+            v0, // visibility of previous point
+            v00, // visibility of first point
+            clean; // no intersections
+        return {
+          lineStart: function() {
+            v00 = v0 = false;
+            clean = 1;
+          },
+          point: function(lambda, phi) {
+            var point1 = [lambda, phi],
+                point2,
+                v = visible(lambda, phi),
+                c = smallRadius
+                  ? v ? 0 : code(lambda, phi)
+                  : v ? code(lambda + (lambda < 0 ? pi$1 : -pi$1), phi) : 0;
+            if (!point0 && (v00 = v0 = v)) stream.lineStart();
+            if (v !== v0) {
+              point2 = intersect(point0, point1);
+              if (!point2 || pointEqual(point0, point2) || pointEqual(point1, point2))
+                point1[2] = 1;
+            }
+            if (v !== v0) {
+              clean = 0;
+              if (v) {
+                // outside going in
+                stream.lineStart();
+                point2 = intersect(point1, point0);
+                stream.point(point2[0], point2[1]);
+              } else {
+                // inside going out
+                point2 = intersect(point0, point1);
+                stream.point(point2[0], point2[1], 2);
+                stream.lineEnd();
+              }
+              point0 = point2;
+            } else if (notHemisphere && point0 && smallRadius ^ v) {
+              var t;
+              // If the codes for two points are different, or are both zero,
+              // and there this segment intersects with the small circle.
+              if (!(c & c0) && (t = intersect(point1, point0, true))) {
+                clean = 0;
+                if (smallRadius) {
+                  stream.lineStart();
+                  stream.point(t[0][0], t[0][1]);
+                  stream.point(t[1][0], t[1][1]);
+                  stream.lineEnd();
+                } else {
+                  stream.point(t[1][0], t[1][1]);
+                  stream.lineEnd();
+                  stream.lineStart();
+                  stream.point(t[0][0], t[0][1], 3);
+                }
+              }
+            }
+            if (v && (!point0 || !pointEqual(point0, point1))) {
+              stream.point(point1[0], point1[1]);
+            }
+            point0 = point1, v0 = v, c0 = c;
+          },
+          lineEnd: function() {
+            if (v0) stream.lineEnd();
+            point0 = null;
+          },
+          // Rejoin first and last segments if there were intersections and the first
+          // and last points were visible.
+          clean: function() {
+            return clean | ((v00 && v0) << 1);
+          }
+        };
+      }
+    
+      // Intersects the great circle between a and b with the clip circle.
+      function intersect(a, b, two) {
+        var pa = cartesian(a),
+            pb = cartesian(b);
+    
+        // We have two planes, n1.p = d1 and n2.p = d2.
+        // Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 ⨯ n2).
+        var n1 = [1, 0, 0], // normal
+            n2 = cartesianCross(pa, pb),
+            n2n2 = cartesianDot(n2, n2),
+            n1n2 = n2[0], // cartesianDot(n1, n2),
+            determinant = n2n2 - n1n2 * n1n2;
+    
+        // Two polar points.
+        if (!determinant) return !two && a;
+    
+        var c1 =  cr * n2n2 / determinant,
+            c2 = -cr * n1n2 / determinant,
+            n1xn2 = cartesianCross(n1, n2),
+            A = cartesianScale(n1, c1),
+            B = cartesianScale(n2, c2);
+        cartesianAddInPlace(A, B);
+    
+        // Solve |p(t)|^2 = 1.
+        var u = n1xn2,
+            w = cartesianDot(A, u),
+            uu = cartesianDot(u, u),
+            t2 = w * w - uu * (cartesianDot(A, A) - 1);
+    
+        if (t2 < 0) return;
+    
+        var t = sqrt$2(t2),
+            q = cartesianScale(u, (-w - t) / uu);
+        cartesianAddInPlace(q, A);
+        q = spherical(q);
+    
+        if (!two) return q;
+    
+        // Two intersection points.
+        var lambda0 = a[0],
+            lambda1 = b[0],
+            phi0 = a[1],
+            phi1 = b[1],
+            z;
+    
+        if (lambda1 < lambda0) z = lambda0, lambda0 = lambda1, lambda1 = z;
+    
+        var delta = lambda1 - lambda0,
+            polar = abs$1(delta - pi$1) < epsilon$1,
+            meridian = polar || delta < epsilon$1;
+    
+        if (!polar && phi1 < phi0) z = phi0, phi0 = phi1, phi1 = z;
+    
+        // Check that the first point is between a and b.
+        if (meridian
+            ? polar
+              ? phi0 + phi1 > 0 ^ q[1] < (abs$1(q[0] - lambda0) < epsilon$1 ? phi0 : phi1)
+              : phi0 <= q[1] && q[1] <= phi1
+            : delta > pi$1 ^ (lambda0 <= q[0] && q[0] <= lambda1)) {
+          var q1 = cartesianScale(u, (-w + t) / uu);
+          cartesianAddInPlace(q1, A);
+          return [q, spherical(q1)];
+        }
+      }
+    
+      // Generates a 4-bit vector representing the location of a point relative to
+      // the small circle's bounding box.
+      function code(lambda, phi) {
+        var r = smallRadius ? radius : pi$1 - radius,
+            code = 0;
+        if (lambda < -r) code |= 1; // left
+        else if (lambda > r) code |= 2; // right
+        if (phi < -r) code |= 4; // below
+        else if (phi > r) code |= 8; // above
+        return code;
+      }
+    
+      return clip(visible, clipLine, interpolate, smallRadius ? [0, -radius] : [-pi$1, radius - pi$1]);
+    }
+    
+    function clipLine(a, b, x0, y0, x1, y1) {
+      var ax = a[0],
+          ay = a[1],
+          bx = b[0],
+          by = b[1],
+          t0 = 0,
+          t1 = 1,
+          dx = bx - ax,
+          dy = by - ay,
+          r;
+    
+      r = x0 - ax;
+      if (!dx && r > 0) return;
+      r /= dx;
+      if (dx < 0) {
+        if (r < t0) return;
+        if (r < t1) t1 = r;
+      } else if (dx > 0) {
+        if (r > t1) return;
+        if (r > t0) t0 = r;
+      }
+    
+      r = x1 - ax;
+      if (!dx && r < 0) return;
+      r /= dx;
+      if (dx < 0) {
+        if (r > t1) return;
+        if (r > t0) t0 = r;
+      } else if (dx > 0) {
+        if (r < t0) return;
+        if (r < t1) t1 = r;
+      }
+    
+      r = y0 - ay;
+      if (!dy && r > 0) return;
+      r /= dy;
+      if (dy < 0) {
+        if (r < t0) return;
+        if (r < t1) t1 = r;
+      } else if (dy > 0) {
+        if (r > t1) return;
+        if (r > t0) t0 = r;
+      }
+    
+      r = y1 - ay;
+      if (!dy && r < 0) return;
+      r /= dy;
+      if (dy < 0) {
+        if (r > t1) return;
+        if (r > t0) t0 = r;
+      } else if (dy > 0) {
+        if (r < t0) return;
+        if (r < t1) t1 = r;
+      }
+    
+      if (t0 > 0) a[0] = ax + t0 * dx, a[1] = ay + t0 * dy;
+      if (t1 < 1) b[0] = ax + t1 * dx, b[1] = ay + t1 * dy;
+      return true;
+    }
+    
+    var clipMax = 1e9, clipMin = -clipMax;
+    
+    // TODO Use d3-polygon’s polygonContains here for the ring check?
+    // TODO Eliminate duplicate buffering in clipBuffer and polygon.push?
+    
+    function clipRectangle(x0, y0, x1, y1) {
+    
+      function visible(x, y) {
+        return x0 <= x && x <= x1 && y0 <= y && y <= y1;
+      }
+    
+      function interpolate(from, to, direction, stream) {
+        var a = 0, a1 = 0;
+        if (from == null
+            || (a = corner(from, direction)) !== (a1 = corner(to, direction))
+            || comparePoint(from, to) < 0 ^ direction > 0) {
+          do stream.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);
+          while ((a = (a + direction + 4) % 4) !== a1);
+        } else {
+          stream.point(to[0], to[1]);
+        }
+      }
+    
+      function corner(p, direction) {
+        return abs$1(p[0] - x0) < epsilon$1 ? direction > 0 ? 0 : 3
+            : abs$1(p[0] - x1) < epsilon$1 ? direction > 0 ? 2 : 1
+            : abs$1(p[1] - y0) < epsilon$1 ? direction > 0 ? 1 : 0
+            : direction > 0 ? 3 : 2; // abs(p[1] - y1) < epsilon
+      }
+    
+      function compareIntersection(a, b) {
+        return comparePoint(a.x, b.x);
+      }
+    
+      function comparePoint(a, b) {
+        var ca = corner(a, 1),
+            cb = corner(b, 1);
+        return ca !== cb ? ca - cb
+            : ca === 0 ? b[1] - a[1]
+            : ca === 1 ? a[0] - b[0]
+            : ca === 2 ? a[1] - b[1]
+            : b[0] - a[0];
+      }
+    
+      return function(stream) {
+        var activeStream = stream,
+            bufferStream = clipBuffer(),
+            segments,
+            polygon,
+            ring,
+            x__, y__, v__, // first point
+            x_, y_, v_, // previous point
+            first,
+            clean;
+    
+        var clipStream = {
+          point: point,
+          lineStart: lineStart,
+          lineEnd: lineEnd,
+          polygonStart: polygonStart,
+          polygonEnd: polygonEnd
+        };
+    
+        function point(x, y) {
+          if (visible(x, y)) activeStream.point(x, y);
+        }
+    
+        function polygonInside() {
+          var winding = 0;
+    
+          for (var i = 0, n = polygon.length; i < n; ++i) {
+            for (var ring = polygon[i], j = 1, m = ring.length, point = ring[0], a0, a1, b0 = point[0], b1 = point[1]; j < m; ++j) {
+              a0 = b0, a1 = b1, point = ring[j], b0 = point[0], b1 = point[1];
+              if (a1 <= y1) { if (b1 > y1 && (b0 - a0) * (y1 - a1) > (b1 - a1) * (x0 - a0)) ++winding; }
+              else { if (b1 <= y1 && (b0 - a0) * (y1 - a1) < (b1 - a1) * (x0 - a0)) --winding; }
+            }
+          }
+    
+          return winding;
+        }
+    
+        // Buffer geometry within a polygon and then clip it en masse.
+        function polygonStart() {
+          activeStream = bufferStream, segments = [], polygon = [], clean = true;
+        }
+    
+        function polygonEnd() {
+          var startInside = polygonInside(),
+              cleanInside = clean && startInside,
+              visible = (segments = merge(segments)).length;
+          if (cleanInside || visible) {
+            stream.polygonStart();
+            if (cleanInside) {
+              stream.lineStart();
+              interpolate(null, null, 1, stream);
+              stream.lineEnd();
+            }
+            if (visible) {
+              clipRejoin(segments, compareIntersection, startInside, interpolate, stream);
+            }
+            stream.polygonEnd();
+          }
+          activeStream = stream, segments = polygon = ring = null;
+        }
+    
+        function lineStart() {
+          clipStream.point = linePoint;
+          if (polygon) polygon.push(ring = []);
+          first = true;
+          v_ = false;
+          x_ = y_ = NaN;
+        }
+    
+        // TODO rather than special-case polygons, simply handle them separately.
+        // Ideally, coincident intersection points should be jittered to avoid
+        // clipping issues.
+        function lineEnd() {
+          if (segments) {
+            linePoint(x__, y__);
+            if (v__ && v_) bufferStream.rejoin();
+            segments.push(bufferStream.result());
+          }
+          clipStream.point = point;
+          if (v_) activeStream.lineEnd();
+        }
+    
+        function linePoint(x, y) {
+          var v = visible(x, y);
+          if (polygon) ring.push([x, y]);
+          if (first) {
+            x__ = x, y__ = y, v__ = v;
+            first = false;
+            if (v) {
+              activeStream.lineStart();
+              activeStream.point(x, y);
+            }
+          } else {
+            if (v && v_) activeStream.point(x, y);
+            else {
+              var a = [x_ = Math.max(clipMin, Math.min(clipMax, x_)), y_ = Math.max(clipMin, Math.min(clipMax, y_))],
+                  b = [x = Math.max(clipMin, Math.min(clipMax, x)), y = Math.max(clipMin, Math.min(clipMax, y))];
+              if (clipLine(a, b, x0, y0, x1, y1)) {
+                if (!v_) {
+                  activeStream.lineStart();
+                  activeStream.point(a[0], a[1]);
+                }
+                activeStream.point(b[0], b[1]);
+                if (!v) activeStream.lineEnd();
+                clean = false;
+              } else if (v) {
+                activeStream.lineStart();
+                activeStream.point(x, y);
+                clean = false;
+              }
+            }
+          }
+          x_ = x, y_ = y, v_ = v;
+        }
+    
+        return clipStream;
+      };
+    }
+    
+    function extent() {
+      var x0 = 0,
+          y0 = 0,
+          x1 = 960,
+          y1 = 500,
+          cache,
+          cacheStream,
+          clip;
+    
+      return clip = {
+        stream: function(stream) {
+          return cache && cacheStream === stream ? cache : cache = clipRectangle(x0, y0, x1, y1)(cacheStream = stream);
+        },
+        extent: function(_) {
+          return arguments.length ? (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1], cache = cacheStream = null, clip) : [[x0, y0], [x1, y1]];
+        }
+      };
+    }
+    
+    var lengthSum$1,
+        lambda0,
+        sinPhi0,
+        cosPhi0;
+    
+    var lengthStream$1 = {
+      sphere: noop$1,
+      point: noop$1,
+      lineStart: lengthLineStart,
+      lineEnd: noop$1,
+      polygonStart: noop$1,
+      polygonEnd: noop$1
+    };
+    
+    function lengthLineStart() {
+      lengthStream$1.point = lengthPointFirst$1;
+      lengthStream$1.lineEnd = lengthLineEnd;
+    }
+    
+    function lengthLineEnd() {
+      lengthStream$1.point = lengthStream$1.lineEnd = noop$1;
+    }
+    
+    function lengthPointFirst$1(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      lambda0 = lambda, sinPhi0 = sin$1(phi), cosPhi0 = cos$1(phi);
+      lengthStream$1.point = lengthPoint$1;
+    }
+    
+    function lengthPoint$1(lambda, phi) {
+      lambda *= radians, phi *= radians;
+      var sinPhi = sin$1(phi),
+          cosPhi = cos$1(phi),
+          delta = abs$1(lambda - lambda0),
+          cosDelta = cos$1(delta),
+          sinDelta = sin$1(delta),
+          x = cosPhi * sinDelta,
+          y = cosPhi0 * sinPhi - sinPhi0 * cosPhi * cosDelta,
+          z = sinPhi0 * sinPhi + cosPhi0 * cosPhi * cosDelta;
+      lengthSum$1.add(atan2$1(sqrt$2(x * x + y * y), z));
+      lambda0 = lambda, sinPhi0 = sinPhi, cosPhi0 = cosPhi;
+    }
+    
+    function length$1(object) {
+      lengthSum$1 = new Adder();
+      geoStream(object, lengthStream$1);
+      return +lengthSum$1;
+    }
+    
+    var coordinates = [null, null],
+        object = {type: "LineString", coordinates: coordinates};
+    
+    function distance(a, b) {
+      coordinates[0] = a;
+      coordinates[1] = b;
+      return length$1(object);
+    }
+    
+    var containsObjectType = {
+      Feature: function(object, point) {
+        return containsGeometry(object.geometry, point);
+      },
+      FeatureCollection: function(object, point) {
+        var features = object.features, i = -1, n = features.length;
+        while (++i < n) if (containsGeometry(features[i].geometry, point)) return true;
+        return false;
+      }
+    };
+    
+    var containsGeometryType = {
+      Sphere: function() {
+        return true;
+      },
+      Point: function(object, point) {
+        return containsPoint(object.coordinates, point);
+      },
+      MultiPoint: function(object, point) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) if (containsPoint(coordinates[i], point)) return true;
+        return false;
+      },
+      LineString: function(object, point) {
+        return containsLine(object.coordinates, point);
+      },
+      MultiLineString: function(object, point) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) if (containsLine(coordinates[i], point)) return true;
+        return false;
+      },
+      Polygon: function(object, point) {
+        return containsPolygon(object.coordinates, point);
+      },
+      MultiPolygon: function(object, point) {
+        var coordinates = object.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) if (containsPolygon(coordinates[i], point)) return true;
+        return false;
+      },
+      GeometryCollection: function(object, point) {
+        var geometries = object.geometries, i = -1, n = geometries.length;
+        while (++i < n) if (containsGeometry(geometries[i], point)) return true;
+        return false;
+      }
+    };
+    
+    function containsGeometry(geometry, point) {
+      return geometry && containsGeometryType.hasOwnProperty(geometry.type)
+          ? containsGeometryType[geometry.type](geometry, point)
+          : false;
+    }
+    
+    function containsPoint(coordinates, point) {
+      return distance(coordinates, point) === 0;
+    }
+    
+    function containsLine(coordinates, point) {
+      var ao, bo, ab;
+      for (var i = 0, n = coordinates.length; i < n; i++) {
+        bo = distance(coordinates[i], point);
+        if (bo === 0) return true;
+        if (i > 0) {
+          ab = distance(coordinates[i], coordinates[i - 1]);
+          if (
+            ab > 0 &&
+            ao <= ab &&
+            bo <= ab &&
+            (ao + bo - ab) * (1 - Math.pow((ao - bo) / ab, 2)) < epsilon2 * ab
+          )
+            return true;
+        }
+        ao = bo;
+      }
+      return false;
+    }
+    
+    function containsPolygon(coordinates, point) {
+      return !!polygonContains(coordinates.map(ringRadians), pointRadians(point));
+    }
+    
+    function ringRadians(ring) {
+      return ring = ring.map(pointRadians), ring.pop(), ring;
+    }
+    
+    function pointRadians(point) {
+      return [point[0] * radians, point[1] * radians];
+    }
+    
+    function contains$1(object, point) {
+      return (object && containsObjectType.hasOwnProperty(object.type)
+          ? containsObjectType[object.type]
+          : containsGeometry)(object, point);
+    }
+    
+    function graticuleX(y0, y1, dy) {
+      var y = sequence(y0, y1 - epsilon$1, dy).concat(y1);
+      return function(x) { return y.map(function(y) { return [x, y]; }); };
+    }
+    
+    function graticuleY(x0, x1, dx) {
+      var x = sequence(x0, x1 - epsilon$1, dx).concat(x1);
+      return function(y) { return x.map(function(x) { return [x, y]; }); };
+    }
+    
+    function graticule() {
+      var x1, x0, X1, X0,
+          y1, y0, Y1, Y0,
+          dx = 10, dy = dx, DX = 90, DY = 360,
+          x, y, X, Y,
+          precision = 2.5;
+    
+      function graticule() {
+        return {type: "MultiLineString", coordinates: lines()};
+      }
+    
+      function lines() {
+        return sequence(ceil(X0 / DX) * DX, X1, DX).map(X)
+            .concat(sequence(ceil(Y0 / DY) * DY, Y1, DY).map(Y))
+            .concat(sequence(ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return abs$1(x % DX) > epsilon$1; }).map(x))
+            .concat(sequence(ceil(y0 / dy) * dy, y1, dy).filter(function(y) { return abs$1(y % DY) > epsilon$1; }).map(y));
+      }
+    
+      graticule.lines = function() {
+        return lines().map(function(coordinates) { return {type: "LineString", coordinates: coordinates}; });
+      };
+    
+      graticule.outline = function() {
+        return {
+          type: "Polygon",
+          coordinates: [
+            X(X0).concat(
+            Y(Y1).slice(1),
+            X(X1).reverse().slice(1),
+            Y(Y0).reverse().slice(1))
+          ]
+        };
+      };
+    
+      graticule.extent = function(_) {
+        if (!arguments.length) return graticule.extentMinor();
+        return graticule.extentMajor(_).extentMinor(_);
+      };
+    
+      graticule.extentMajor = function(_) {
+        if (!arguments.length) return [[X0, Y0], [X1, Y1]];
+        X0 = +_[0][0], X1 = +_[1][0];
+        Y0 = +_[0][1], Y1 = +_[1][1];
+        if (X0 > X1) _ = X0, X0 = X1, X1 = _;
+        if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;
+        return graticule.precision(precision);
+      };
+    
+      graticule.extentMinor = function(_) {
+        if (!arguments.length) return [[x0, y0], [x1, y1]];
+        x0 = +_[0][0], x1 = +_[1][0];
+        y0 = +_[0][1], y1 = +_[1][1];
+        if (x0 > x1) _ = x0, x0 = x1, x1 = _;
+        if (y0 > y1) _ = y0, y0 = y1, y1 = _;
+        return graticule.precision(precision);
+      };
+    
+      graticule.step = function(_) {
+        if (!arguments.length) return graticule.stepMinor();
+        return graticule.stepMajor(_).stepMinor(_);
+      };
+    
+      graticule.stepMajor = function(_) {
+        if (!arguments.length) return [DX, DY];
+        DX = +_[0], DY = +_[1];
+        return graticule;
+      };
+    
+      graticule.stepMinor = function(_) {
+        if (!arguments.length) return [dx, dy];
+        dx = +_[0], dy = +_[1];
+        return graticule;
+      };
+    
+      graticule.precision = function(_) {
+        if (!arguments.length) return precision;
+        precision = +_;
+        x = graticuleX(y0, y1, 90);
+        y = graticuleY(x0, x1, precision);
+        X = graticuleX(Y0, Y1, 90);
+        Y = graticuleY(X0, X1, precision);
+        return graticule;
+      };
+    
+      return graticule
+          .extentMajor([[-180, -90 + epsilon$1], [180, 90 - epsilon$1]])
+          .extentMinor([[-180, -80 - epsilon$1], [180, 80 + epsilon$1]]);
+    }
+    
+    function graticule10() {
+      return graticule()();
+    }
+    
+    function interpolate(a, b) {
+      var x0 = a[0] * radians,
+          y0 = a[1] * radians,
+          x1 = b[0] * radians,
+          y1 = b[1] * radians,
+          cy0 = cos$1(y0),
+          sy0 = sin$1(y0),
+          cy1 = cos$1(y1),
+          sy1 = sin$1(y1),
+          kx0 = cy0 * cos$1(x0),
+          ky0 = cy0 * sin$1(x0),
+          kx1 = cy1 * cos$1(x1),
+          ky1 = cy1 * sin$1(x1),
+          d = 2 * asin$1(sqrt$2(haversin(y1 - y0) + cy0 * cy1 * haversin(x1 - x0))),
+          k = sin$1(d);
+    
+      var interpolate = d ? function(t) {
+        var B = sin$1(t *= d) / k,
+            A = sin$1(d - t) / k,
+            x = A * kx0 + B * kx1,
+            y = A * ky0 + B * ky1,
+            z = A * sy0 + B * sy1;
+        return [
+          atan2$1(y, x) * degrees,
+          atan2$1(z, sqrt$2(x * x + y * y)) * degrees
+        ];
+      } : function() {
+        return [x0 * degrees, y0 * degrees];
+      };
+    
+      interpolate.distance = d;
+    
+      return interpolate;
+    }
+    
+    var identity$5 = x => x;
+    
+    var areaSum = new Adder(),
+        areaRingSum = new Adder(),
+        x00$2,
+        y00$2,
+        x0$3,
+        y0$3;
+    
+    var areaStream = {
+      point: noop$1,
+      lineStart: noop$1,
+      lineEnd: noop$1,
+      polygonStart: function() {
+        areaStream.lineStart = areaRingStart;
+        areaStream.lineEnd = areaRingEnd;
+      },
+      polygonEnd: function() {
+        areaStream.lineStart = areaStream.lineEnd = areaStream.point = noop$1;
+        areaSum.add(abs$1(areaRingSum));
+        areaRingSum = new Adder();
+      },
+      result: function() {
+        var area = areaSum / 2;
+        areaSum = new Adder();
+        return area;
+      }
+    };
+    
+    function areaRingStart() {
+      areaStream.point = areaPointFirst;
+    }
+    
+    function areaPointFirst(x, y) {
+      areaStream.point = areaPoint;
+      x00$2 = x0$3 = x, y00$2 = y0$3 = y;
+    }
+    
+    function areaPoint(x, y) {
+      areaRingSum.add(y0$3 * x - x0$3 * y);
+      x0$3 = x, y0$3 = y;
+    }
+    
+    function areaRingEnd() {
+      areaPoint(x00$2, y00$2);
+    }
+    
+    var x0$2 = Infinity,
+        y0$2 = x0$2,
+        x1 = -x0$2,
+        y1 = x1;
+    
+    var boundsStream = {
+      point: boundsPoint,
+      lineStart: noop$1,
+      lineEnd: noop$1,
+      polygonStart: noop$1,
+      polygonEnd: noop$1,
+      result: function() {
+        var bounds = [[x0$2, y0$2], [x1, y1]];
+        x1 = y1 = -(y0$2 = x0$2 = Infinity);
+        return bounds;
+      }
+    };
+    
+    function boundsPoint(x, y) {
+      if (x < x0$2) x0$2 = x;
+      if (x > x1) x1 = x;
+      if (y < y0$2) y0$2 = y;
+      if (y > y1) y1 = y;
+    }
+    
+    // TODO Enforce positive area for exterior, negative area for interior?
+    
+    var X0 = 0,
+        Y0 = 0,
+        Z0 = 0,
+        X1 = 0,
+        Y1 = 0,
+        Z1 = 0,
+        X2 = 0,
+        Y2 = 0,
+        Z2 = 0,
+        x00$1,
+        y00$1,
+        x0$1,
+        y0$1;
+    
+    var centroidStream = {
+      point: centroidPoint,
+      lineStart: centroidLineStart,
+      lineEnd: centroidLineEnd,
+      polygonStart: function() {
+        centroidStream.lineStart = centroidRingStart;
+        centroidStream.lineEnd = centroidRingEnd;
+      },
+      polygonEnd: function() {
+        centroidStream.point = centroidPoint;
+        centroidStream.lineStart = centroidLineStart;
+        centroidStream.lineEnd = centroidLineEnd;
+      },
+      result: function() {
+        var centroid = Z2 ? [X2 / Z2, Y2 / Z2]
+            : Z1 ? [X1 / Z1, Y1 / Z1]
+            : Z0 ? [X0 / Z0, Y0 / Z0]
+            : [NaN, NaN];
+        X0 = Y0 = Z0 =
+        X1 = Y1 = Z1 =
+        X2 = Y2 = Z2 = 0;
+        return centroid;
+      }
+    };
+    
+    function centroidPoint(x, y) {
+      X0 += x;
+      Y0 += y;
+      ++Z0;
+    }
+    
+    function centroidLineStart() {
+      centroidStream.point = centroidPointFirstLine;
+    }
+    
+    function centroidPointFirstLine(x, y) {
+      centroidStream.point = centroidPointLine;
+      centroidPoint(x0$1 = x, y0$1 = y);
+    }
+    
+    function centroidPointLine(x, y) {
+      var dx = x - x0$1, dy = y - y0$1, z = sqrt$2(dx * dx + dy * dy);
+      X1 += z * (x0$1 + x) / 2;
+      Y1 += z * (y0$1 + y) / 2;
+      Z1 += z;
+      centroidPoint(x0$1 = x, y0$1 = y);
+    }
+    
+    function centroidLineEnd() {
+      centroidStream.point = centroidPoint;
+    }
+    
+    function centroidRingStart() {
+      centroidStream.point = centroidPointFirstRing;
+    }
+    
+    function centroidRingEnd() {
+      centroidPointRing(x00$1, y00$1);
+    }
+    
+    function centroidPointFirstRing(x, y) {
+      centroidStream.point = centroidPointRing;
+      centroidPoint(x00$1 = x0$1 = x, y00$1 = y0$1 = y);
+    }
+    
+    function centroidPointRing(x, y) {
+      var dx = x - x0$1,
+          dy = y - y0$1,
+          z = sqrt$2(dx * dx + dy * dy);
+    
+      X1 += z * (x0$1 + x) / 2;
+      Y1 += z * (y0$1 + y) / 2;
+      Z1 += z;
+    
+      z = y0$1 * x - x0$1 * y;
+      X2 += z * (x0$1 + x);
+      Y2 += z * (y0$1 + y);
+      Z2 += z * 3;
+      centroidPoint(x0$1 = x, y0$1 = y);
+    }
+    
+    function PathContext(context) {
+      this._context = context;
+    }
+    
+    PathContext.prototype = {
+      _radius: 4.5,
+      pointRadius: function(_) {
+        return this._radius = _, this;
+      },
+      polygonStart: function() {
+        this._line = 0;
+      },
+      polygonEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line === 0) this._context.closePath();
+        this._point = NaN;
+      },
+      point: function(x, y) {
+        switch (this._point) {
+          case 0: {
+            this._context.moveTo(x, y);
+            this._point = 1;
+            break;
+          }
+          case 1: {
+            this._context.lineTo(x, y);
+            break;
+          }
+          default: {
+            this._context.moveTo(x + this._radius, y);
+            this._context.arc(x, y, this._radius, 0, tau$1);
+            break;
+          }
+        }
+      },
+      result: noop$1
+    };
+    
+    var lengthSum = new Adder(),
+        lengthRing,
+        x00,
+        y00,
+        x0,
+        y0;
+    
+    var lengthStream = {
+      point: noop$1,
+      lineStart: function() {
+        lengthStream.point = lengthPointFirst;
+      },
+      lineEnd: function() {
+        if (lengthRing) lengthPoint(x00, y00);
+        lengthStream.point = noop$1;
+      },
+      polygonStart: function() {
+        lengthRing = true;
+      },
+      polygonEnd: function() {
+        lengthRing = null;
+      },
+      result: function() {
+        var length = +lengthSum;
+        lengthSum = new Adder();
+        return length;
+      }
+    };
+    
+    function lengthPointFirst(x, y) {
+      lengthStream.point = lengthPoint;
+      x00 = x0 = x, y00 = y0 = y;
+    }
+    
+    function lengthPoint(x, y) {
+      x0 -= x, y0 -= y;
+      lengthSum.add(sqrt$2(x0 * x0 + y0 * y0));
+      x0 = x, y0 = y;
+    }
+    
+    function PathString() {
+      this._string = [];
+    }
+    
+    PathString.prototype = {
+      _radius: 4.5,
+      _circle: circle$1(4.5),
+      pointRadius: function(_) {
+        if ((_ = +_) !== this._radius) this._radius = _, this._circle = null;
+        return this;
+      },
+      polygonStart: function() {
+        this._line = 0;
+      },
+      polygonEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line === 0) this._string.push("Z");
+        this._point = NaN;
+      },
+      point: function(x, y) {
+        switch (this._point) {
+          case 0: {
+            this._string.push("M", x, ",", y);
+            this._point = 1;
+            break;
+          }
+          case 1: {
+            this._string.push("L", x, ",", y);
+            break;
+          }
+          default: {
+            if (this._circle == null) this._circle = circle$1(this._radius);
+            this._string.push("M", x, ",", y, this._circle);
+            break;
+          }
+        }
+      },
+      result: function() {
+        if (this._string.length) {
+          var result = this._string.join("");
+          this._string = [];
+          return result;
+        } else {
+          return null;
+        }
+      }
+    };
+    
+    function circle$1(radius) {
+      return "m0," + radius
+          + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius
+          + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius
+          + "z";
+    }
+    
+    function index$2(projection, context) {
+      var pointRadius = 4.5,
+          projectionStream,
+          contextStream;
+    
+      function path(object) {
+        if (object) {
+          if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments));
+          geoStream(object, projectionStream(contextStream));
+        }
+        return contextStream.result();
+      }
+    
+      path.area = function(object) {
+        geoStream(object, projectionStream(areaStream));
+        return areaStream.result();
+      };
+    
+      path.measure = function(object) {
+        geoStream(object, projectionStream(lengthStream));
+        return lengthStream.result();
+      };
+    
+      path.bounds = function(object) {
+        geoStream(object, projectionStream(boundsStream));
+        return boundsStream.result();
+      };
+    
+      path.centroid = function(object) {
+        geoStream(object, projectionStream(centroidStream));
+        return centroidStream.result();
+      };
+    
+      path.projection = function(_) {
+        return arguments.length ? (projectionStream = _ == null ? (projection = null, identity$5) : (projection = _).stream, path) : projection;
+      };
+    
+      path.context = function(_) {
+        if (!arguments.length) return context;
+        contextStream = _ == null ? (context = null, new PathString) : new PathContext(context = _);
+        if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
+        return path;
+      };
+    
+      path.pointRadius = function(_) {
+        if (!arguments.length) return pointRadius;
+        pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_);
+        return path;
+      };
+    
+      return path.projection(projection).context(context);
+    }
+    
+    function transform$1(methods) {
+      return {
+        stream: transformer$3(methods)
+      };
+    }
+    
+    function transformer$3(methods) {
+      return function(stream) {
+        var s = new TransformStream;
+        for (var key in methods) s[key] = methods[key];
+        s.stream = stream;
+        return s;
+      };
+    }
+    
+    function TransformStream() {}
+    
+    TransformStream.prototype = {
+      constructor: TransformStream,
+      point: function(x, y) { this.stream.point(x, y); },
+      sphere: function() { this.stream.sphere(); },
+      lineStart: function() { this.stream.lineStart(); },
+      lineEnd: function() { this.stream.lineEnd(); },
+      polygonStart: function() { this.stream.polygonStart(); },
+      polygonEnd: function() { this.stream.polygonEnd(); }
+    };
+    
+    function fit(projection, fitBounds, object) {
+      var clip = projection.clipExtent && projection.clipExtent();
+      projection.scale(150).translate([0, 0]);
+      if (clip != null) projection.clipExtent(null);
+      geoStream(object, projection.stream(boundsStream));
+      fitBounds(boundsStream.result());
+      if (clip != null) projection.clipExtent(clip);
+      return projection;
+    }
+    
+    function fitExtent(projection, extent, object) {
+      return fit(projection, function(b) {
+        var w = extent[1][0] - extent[0][0],
+            h = extent[1][1] - extent[0][1],
+            k = Math.min(w / (b[1][0] - b[0][0]), h / (b[1][1] - b[0][1])),
+            x = +extent[0][0] + (w - k * (b[1][0] + b[0][0])) / 2,
+            y = +extent[0][1] + (h - k * (b[1][1] + b[0][1])) / 2;
+        projection.scale(150 * k).translate([x, y]);
+      }, object);
+    }
+    
+    function fitSize(projection, size, object) {
+      return fitExtent(projection, [[0, 0], size], object);
+    }
+    
+    function fitWidth(projection, width, object) {
+      return fit(projection, function(b) {
+        var w = +width,
+            k = w / (b[1][0] - b[0][0]),
+            x = (w - k * (b[1][0] + b[0][0])) / 2,
+            y = -k * b[0][1];
+        projection.scale(150 * k).translate([x, y]);
+      }, object);
+    }
+    
+    function fitHeight(projection, height, object) {
+      return fit(projection, function(b) {
+        var h = +height,
+            k = h / (b[1][1] - b[0][1]),
+            x = -k * b[0][0],
+            y = (h - k * (b[1][1] + b[0][1])) / 2;
+        projection.scale(150 * k).translate([x, y]);
+      }, object);
+    }
+    
+    var maxDepth = 16, // maximum depth of subdivision
+        cosMinDistance = cos$1(30 * radians); // cos(minimum angular distance)
+    
+    function resample(project, delta2) {
+      return +delta2 ? resample$1(project, delta2) : resampleNone(project);
+    }
+    
+    function resampleNone(project) {
+      return transformer$3({
+        point: function(x, y) {
+          x = project(x, y);
+          this.stream.point(x[0], x[1]);
+        }
+      });
+    }
+    
+    function resample$1(project, delta2) {
+    
+      function resampleLineTo(x0, y0, lambda0, a0, b0, c0, x1, y1, lambda1, a1, b1, c1, depth, stream) {
+        var dx = x1 - x0,
+            dy = y1 - y0,
+            d2 = dx * dx + dy * dy;
+        if (d2 > 4 * delta2 && depth--) {
+          var a = a0 + a1,
+              b = b0 + b1,
+              c = c0 + c1,
+              m = sqrt$2(a * a + b * b + c * c),
+              phi2 = asin$1(c /= m),
+              lambda2 = abs$1(abs$1(c) - 1) < epsilon$1 || abs$1(lambda0 - lambda1) < epsilon$1 ? (lambda0 + lambda1) / 2 : atan2$1(b, a),
+              p = project(lambda2, phi2),
+              x2 = p[0],
+              y2 = p[1],
+              dx2 = x2 - x0,
+              dy2 = y2 - y0,
+              dz = dy * dx2 - dx * dy2;
+          if (dz * dz / d2 > delta2 // perpendicular projected distance
+              || abs$1((dx * dx2 + dy * dy2) / d2 - 0.5) > 0.3 // midpoint close to an end
+              || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { // angular distance
+            resampleLineTo(x0, y0, lambda0, a0, b0, c0, x2, y2, lambda2, a /= m, b /= m, c, depth, stream);
+            stream.point(x2, y2);
+            resampleLineTo(x2, y2, lambda2, a, b, c, x1, y1, lambda1, a1, b1, c1, depth, stream);
+          }
+        }
+      }
+      return function(stream) {
+        var lambda00, x00, y00, a00, b00, c00, // first point
+            lambda0, x0, y0, a0, b0, c0; // previous point
+    
+        var resampleStream = {
+          point: point,
+          lineStart: lineStart,
+          lineEnd: lineEnd,
+          polygonStart: function() { stream.polygonStart(); resampleStream.lineStart = ringStart; },
+          polygonEnd: function() { stream.polygonEnd(); resampleStream.lineStart = lineStart; }
+        };
+    
+        function point(x, y) {
+          x = project(x, y);
+          stream.point(x[0], x[1]);
+        }
+    
+        function lineStart() {
+          x0 = NaN;
+          resampleStream.point = linePoint;
+          stream.lineStart();
+        }
+    
+        function linePoint(lambda, phi) {
+          var c = cartesian([lambda, phi]), p = project(lambda, phi);
+          resampleLineTo(x0, y0, lambda0, a0, b0, c0, x0 = p[0], y0 = p[1], lambda0 = lambda, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);
+          stream.point(x0, y0);
+        }
+    
+        function lineEnd() {
+          resampleStream.point = point;
+          stream.lineEnd();
+        }
+    
+        function ringStart() {
+          lineStart();
+          resampleStream.point = ringPoint;
+          resampleStream.lineEnd = ringEnd;
+        }
+    
+        function ringPoint(lambda, phi) {
+          linePoint(lambda00 = lambda, phi), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;
+          resampleStream.point = linePoint;
+        }
+    
+        function ringEnd() {
+          resampleLineTo(x0, y0, lambda0, a0, b0, c0, x00, y00, lambda00, a00, b00, c00, maxDepth, stream);
+          resampleStream.lineEnd = lineEnd;
+          lineEnd();
+        }
+    
+        return resampleStream;
+      };
+    }
+    
+    var transformRadians = transformer$3({
+      point: function(x, y) {
+        this.stream.point(x * radians, y * radians);
+      }
+    });
+    
+    function transformRotate(rotate) {
+      return transformer$3({
+        point: function(x, y) {
+          var r = rotate(x, y);
+          return this.stream.point(r[0], r[1]);
+        }
+      });
+    }
+    
+    function scaleTranslate(k, dx, dy, sx, sy) {
+      function transform(x, y) {
+        x *= sx; y *= sy;
+        return [dx + k * x, dy - k * y];
+      }
+      transform.invert = function(x, y) {
+        return [(x - dx) / k * sx, (dy - y) / k * sy];
+      };
+      return transform;
+    }
+    
+    function scaleTranslateRotate(k, dx, dy, sx, sy, alpha) {
+      if (!alpha) return scaleTranslate(k, dx, dy, sx, sy);
+      var cosAlpha = cos$1(alpha),
+          sinAlpha = sin$1(alpha),
+          a = cosAlpha * k,
+          b = sinAlpha * k,
+          ai = cosAlpha / k,
+          bi = sinAlpha / k,
+          ci = (sinAlpha * dy - cosAlpha * dx) / k,
+          fi = (sinAlpha * dx + cosAlpha * dy) / k;
+      function transform(x, y) {
+        x *= sx; y *= sy;
+        return [a * x - b * y + dx, dy - b * x - a * y];
+      }
+      transform.invert = function(x, y) {
+        return [sx * (ai * x - bi * y + ci), sy * (fi - bi * x - ai * y)];
+      };
+      return transform;
+    }
+    
+    function projection(project) {
+      return projectionMutator(function() { return project; })();
+    }
+    
+    function projectionMutator(projectAt) {
+      var project,
+          k = 150, // scale
+          x = 480, y = 250, // translate
+          lambda = 0, phi = 0, // center
+          deltaLambda = 0, deltaPhi = 0, deltaGamma = 0, rotate, // pre-rotate
+          alpha = 0, // post-rotate angle
+          sx = 1, // reflectX
+          sy = 1, // reflectX
+          theta = null, preclip = clipAntimeridian, // pre-clip angle
+          x0 = null, y0, x1, y1, postclip = identity$5, // post-clip extent
+          delta2 = 0.5, // precision
+          projectResample,
+          projectTransform,
+          projectRotateTransform,
+          cache,
+          cacheStream;
+    
+      function projection(point) {
+        return projectRotateTransform(point[0] * radians, point[1] * radians);
+      }
+    
+      function invert(point) {
+        point = projectRotateTransform.invert(point[0], point[1]);
+        return point && [point[0] * degrees, point[1] * degrees];
+      }
+    
+      projection.stream = function(stream) {
+        return cache && cacheStream === stream ? cache : cache = transformRadians(transformRotate(rotate)(preclip(projectResample(postclip(cacheStream = stream)))));
+      };
+    
+      projection.preclip = function(_) {
+        return arguments.length ? (preclip = _, theta = undefined, reset()) : preclip;
+      };
+    
+      projection.postclip = function(_) {
+        return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;
+      };
+    
+      projection.clipAngle = function(_) {
+        return arguments.length ? (preclip = +_ ? clipCircle(theta = _ * radians) : (theta = null, clipAntimeridian), reset()) : theta * degrees;
+      };
+    
+      projection.clipExtent = function(_) {
+        return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity$5) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+      };
+    
+      projection.scale = function(_) {
+        return arguments.length ? (k = +_, recenter()) : k;
+      };
+    
+      projection.translate = function(_) {
+        return arguments.length ? (x = +_[0], y = +_[1], recenter()) : [x, y];
+      };
+    
+      projection.center = function(_) {
+        return arguments.length ? (lambda = _[0] % 360 * radians, phi = _[1] % 360 * radians, recenter()) : [lambda * degrees, phi * degrees];
+      };
+    
+      projection.rotate = function(_) {
+        return arguments.length ? (deltaLambda = _[0] % 360 * radians, deltaPhi = _[1] % 360 * radians, deltaGamma = _.length > 2 ? _[2] % 360 * radians : 0, recenter()) : [deltaLambda * degrees, deltaPhi * degrees, deltaGamma * degrees];
+      };
+    
+      projection.angle = function(_) {
+        return arguments.length ? (alpha = _ % 360 * radians, recenter()) : alpha * degrees;
+      };
+    
+      projection.reflectX = function(_) {
+        return arguments.length ? (sx = _ ? -1 : 1, recenter()) : sx < 0;
+      };
+    
+      projection.reflectY = function(_) {
+        return arguments.length ? (sy = _ ? -1 : 1, recenter()) : sy < 0;
+      };
+    
+      projection.precision = function(_) {
+        return arguments.length ? (projectResample = resample(projectTransform, delta2 = _ * _), reset()) : sqrt$2(delta2);
+      };
+    
+      projection.fitExtent = function(extent, object) {
+        return fitExtent(projection, extent, object);
+      };
+    
+      projection.fitSize = function(size, object) {
+        return fitSize(projection, size, object);
+      };
+    
+      projection.fitWidth = function(width, object) {
+        return fitWidth(projection, width, object);
+      };
+    
+      projection.fitHeight = function(height, object) {
+        return fitHeight(projection, height, object);
+      };
+    
+      function recenter() {
+        var center = scaleTranslateRotate(k, 0, 0, sx, sy, alpha).apply(null, project(lambda, phi)),
+            transform = scaleTranslateRotate(k, x - center[0], y - center[1], sx, sy, alpha);
+        rotate = rotateRadians(deltaLambda, deltaPhi, deltaGamma);
+        projectTransform = compose(project, transform);
+        projectRotateTransform = compose(rotate, projectTransform);
+        projectResample = resample(projectTransform, delta2);
+        return reset();
+      }
+    
+      function reset() {
+        cache = cacheStream = null;
+        return projection;
+      }
+    
+      return function() {
+        project = projectAt.apply(this, arguments);
+        projection.invert = project.invert && invert;
+        return recenter();
+      };
+    }
+    
+    function conicProjection(projectAt) {
+      var phi0 = 0,
+          phi1 = pi$1 / 3,
+          m = projectionMutator(projectAt),
+          p = m(phi0, phi1);
+    
+      p.parallels = function(_) {
+        return arguments.length ? m(phi0 = _[0] * radians, phi1 = _[1] * radians) : [phi0 * degrees, phi1 * degrees];
+      };
+    
+      return p;
+    }
+    
+    function cylindricalEqualAreaRaw(phi0) {
+      var cosPhi0 = cos$1(phi0);
+    
+      function forward(lambda, phi) {
+        return [lambda * cosPhi0, sin$1(phi) / cosPhi0];
+      }
+    
+      forward.invert = function(x, y) {
+        return [x / cosPhi0, asin$1(y * cosPhi0)];
+      };
+    
+      return forward;
+    }
+    
+    function conicEqualAreaRaw(y0, y1) {
+      var sy0 = sin$1(y0), n = (sy0 + sin$1(y1)) / 2;
+    
+      // Are the parallels symmetrical around the Equator?
+      if (abs$1(n) < epsilon$1) return cylindricalEqualAreaRaw(y0);
+    
+      var c = 1 + sy0 * (2 * n - sy0), r0 = sqrt$2(c) / n;
+    
+      function project(x, y) {
+        var r = sqrt$2(c - 2 * n * sin$1(y)) / n;
+        return [r * sin$1(x *= n), r0 - r * cos$1(x)];
+      }
+    
+      project.invert = function(x, y) {
+        var r0y = r0 - y,
+            l = atan2$1(x, abs$1(r0y)) * sign$1(r0y);
+        if (r0y * n < 0)
+          l -= pi$1 * sign$1(x) * sign$1(r0y);
+        return [l / n, asin$1((c - (x * x + r0y * r0y) * n * n) / (2 * n))];
+      };
+    
+      return project;
+    }
+    
+    function conicEqualArea() {
+      return conicProjection(conicEqualAreaRaw)
+          .scale(155.424)
+          .center([0, 33.6442]);
+    }
+    
+    function albers() {
+      return conicEqualArea()
+          .parallels([29.5, 45.5])
+          .scale(1070)
+          .translate([480, 250])
+          .rotate([96, 0])
+          .center([-0.6, 38.7]);
+    }
+    
+    // The projections must have mutually exclusive clip regions on the sphere,
+    // as this will avoid emitting interleaving lines and polygons.
+    function multiplex(streams) {
+      var n = streams.length;
+      return {
+        point: function(x, y) { var i = -1; while (++i < n) streams[i].point(x, y); },
+        sphere: function() { var i = -1; while (++i < n) streams[i].sphere(); },
+        lineStart: function() { var i = -1; while (++i < n) streams[i].lineStart(); },
+        lineEnd: function() { var i = -1; while (++i < n) streams[i].lineEnd(); },
+        polygonStart: function() { var i = -1; while (++i < n) streams[i].polygonStart(); },
+        polygonEnd: function() { var i = -1; while (++i < n) streams[i].polygonEnd(); }
+      };
+    }
+    
+    // A composite projection for the United States, configured by default for
+    // 960×500. The projection also works quite well at 960×600 if you change the
+    // scale to 1285 and adjust the translate accordingly. The set of standard
+    // parallels for each region comes from USGS, which is published here:
+    // http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers
+    function albersUsa() {
+      var cache,
+          cacheStream,
+          lower48 = albers(), lower48Point,
+          alaska = conicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]), alaskaPoint, // EPSG:3338
+          hawaii = conicEqualArea().rotate([157, 0]).center([-3, 19.9]).parallels([8, 18]), hawaiiPoint, // ESRI:102007
+          point, pointStream = {point: function(x, y) { point = [x, y]; }};
+    
+      function albersUsa(coordinates) {
+        var x = coordinates[0], y = coordinates[1];
+        return point = null,
+            (lower48Point.point(x, y), point)
+            || (alaskaPoint.point(x, y), point)
+            || (hawaiiPoint.point(x, y), point);
+      }
+    
+      albersUsa.invert = function(coordinates) {
+        var k = lower48.scale(),
+            t = lower48.translate(),
+            x = (coordinates[0] - t[0]) / k,
+            y = (coordinates[1] - t[1]) / k;
+        return (y >= 0.120 && y < 0.234 && x >= -0.425 && x < -0.214 ? alaska
+            : y >= 0.166 && y < 0.234 && x >= -0.214 && x < -0.115 ? hawaii
+            : lower48).invert(coordinates);
+      };
+    
+      albersUsa.stream = function(stream) {
+        return cache && cacheStream === stream ? cache : cache = multiplex([lower48.stream(cacheStream = stream), alaska.stream(stream), hawaii.stream(stream)]);
+      };
+    
+      albersUsa.precision = function(_) {
+        if (!arguments.length) return lower48.precision();
+        lower48.precision(_), alaska.precision(_), hawaii.precision(_);
+        return reset();
+      };
+    
+      albersUsa.scale = function(_) {
+        if (!arguments.length) return lower48.scale();
+        lower48.scale(_), alaska.scale(_ * 0.35), hawaii.scale(_);
+        return albersUsa.translate(lower48.translate());
+      };
+    
+      albersUsa.translate = function(_) {
+        if (!arguments.length) return lower48.translate();
+        var k = lower48.scale(), x = +_[0], y = +_[1];
+    
+        lower48Point = lower48
+            .translate(_)
+            .clipExtent([[x - 0.455 * k, y - 0.238 * k], [x + 0.455 * k, y + 0.238 * k]])
+            .stream(pointStream);
+    
+        alaskaPoint = alaska
+            .translate([x - 0.307 * k, y + 0.201 * k])
+            .clipExtent([[x - 0.425 * k + epsilon$1, y + 0.120 * k + epsilon$1], [x - 0.214 * k - epsilon$1, y + 0.234 * k - epsilon$1]])
+            .stream(pointStream);
+    
+        hawaiiPoint = hawaii
+            .translate([x - 0.205 * k, y + 0.212 * k])
+            .clipExtent([[x - 0.214 * k + epsilon$1, y + 0.166 * k + epsilon$1], [x - 0.115 * k - epsilon$1, y + 0.234 * k - epsilon$1]])
+            .stream(pointStream);
+    
+        return reset();
+      };
+    
+      albersUsa.fitExtent = function(extent, object) {
+        return fitExtent(albersUsa, extent, object);
+      };
+    
+      albersUsa.fitSize = function(size, object) {
+        return fitSize(albersUsa, size, object);
+      };
+    
+      albersUsa.fitWidth = function(width, object) {
+        return fitWidth(albersUsa, width, object);
+      };
+    
+      albersUsa.fitHeight = function(height, object) {
+        return fitHeight(albersUsa, height, object);
+      };
+    
+      function reset() {
+        cache = cacheStream = null;
+        return albersUsa;
+      }
+    
+      return albersUsa.scale(1070);
+    }
+    
+    function azimuthalRaw(scale) {
+      return function(x, y) {
+        var cx = cos$1(x),
+            cy = cos$1(y),
+            k = scale(cx * cy);
+            if (k === Infinity) return [2, 0];
+        return [
+          k * cy * sin$1(x),
+          k * sin$1(y)
+        ];
+      }
+    }
+    
+    function azimuthalInvert(angle) {
+      return function(x, y) {
+        var z = sqrt$2(x * x + y * y),
+            c = angle(z),
+            sc = sin$1(c),
+            cc = cos$1(c);
+        return [
+          atan2$1(x * sc, z * cc),
+          asin$1(z && y * sc / z)
+        ];
+      }
+    }
+    
+    var azimuthalEqualAreaRaw = azimuthalRaw(function(cxcy) {
+      return sqrt$2(2 / (1 + cxcy));
+    });
+    
+    azimuthalEqualAreaRaw.invert = azimuthalInvert(function(z) {
+      return 2 * asin$1(z / 2);
+    });
+    
+    function azimuthalEqualArea() {
+      return projection(azimuthalEqualAreaRaw)
+          .scale(124.75)
+          .clipAngle(180 - 1e-3);
+    }
+    
+    var azimuthalEquidistantRaw = azimuthalRaw(function(c) {
+      return (c = acos$1(c)) && c / sin$1(c);
+    });
+    
+    azimuthalEquidistantRaw.invert = azimuthalInvert(function(z) {
+      return z;
+    });
+    
+    function azimuthalEquidistant() {
+      return projection(azimuthalEquidistantRaw)
+          .scale(79.4188)
+          .clipAngle(180 - 1e-3);
+    }
+    
+    function mercatorRaw(lambda, phi) {
+      return [lambda, log$1(tan((halfPi$1 + phi) / 2))];
+    }
+    
+    mercatorRaw.invert = function(x, y) {
+      return [x, 2 * atan(exp(y)) - halfPi$1];
+    };
+    
+    function mercator() {
+      return mercatorProjection(mercatorRaw)
+          .scale(961 / tau$1);
+    }
+    
+    function mercatorProjection(project) {
+      var m = projection(project),
+          center = m.center,
+          scale = m.scale,
+          translate = m.translate,
+          clipExtent = m.clipExtent,
+          x0 = null, y0, x1, y1; // clip extent
+    
+      m.scale = function(_) {
+        return arguments.length ? (scale(_), reclip()) : scale();
+      };
+    
+      m.translate = function(_) {
+        return arguments.length ? (translate(_), reclip()) : translate();
+      };
+    
+      m.center = function(_) {
+        return arguments.length ? (center(_), reclip()) : center();
+      };
+    
+      m.clipExtent = function(_) {
+        return arguments.length ? ((_ == null ? x0 = y0 = x1 = y1 = null : (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1])), reclip()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+      };
+    
+      function reclip() {
+        var k = pi$1 * scale(),
+            t = m(rotation(m.rotate()).invert([0, 0]));
+        return clipExtent(x0 == null
+            ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] : project === mercatorRaw
+            ? [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]]
+            : [[x0, Math.max(t[1] - k, y0)], [x1, Math.min(t[1] + k, y1)]]);
+      }
+    
+      return reclip();
+    }
+    
+    function tany(y) {
+      return tan((halfPi$1 + y) / 2);
+    }
+    
+    function conicConformalRaw(y0, y1) {
+      var cy0 = cos$1(y0),
+          n = y0 === y1 ? sin$1(y0) : log$1(cy0 / cos$1(y1)) / log$1(tany(y1) / tany(y0)),
+          f = cy0 * pow$1(tany(y0), n) / n;
+    
+      if (!n) return mercatorRaw;
+    
+      function project(x, y) {
+        if (f > 0) { if (y < -halfPi$1 + epsilon$1) y = -halfPi$1 + epsilon$1; }
+        else { if (y > halfPi$1 - epsilon$1) y = halfPi$1 - epsilon$1; }
+        var r = f / pow$1(tany(y), n);
+        return [r * sin$1(n * x), f - r * cos$1(n * x)];
+      }
+    
+      project.invert = function(x, y) {
+        var fy = f - y, r = sign$1(n) * sqrt$2(x * x + fy * fy),
+          l = atan2$1(x, abs$1(fy)) * sign$1(fy);
+        if (fy * n < 0)
+          l -= pi$1 * sign$1(x) * sign$1(fy);
+        return [l / n, 2 * atan(pow$1(f / r, 1 / n)) - halfPi$1];
+      };
+    
+      return project;
+    }
+    
+    function conicConformal() {
+      return conicProjection(conicConformalRaw)
+          .scale(109.5)
+          .parallels([30, 30]);
+    }
+    
+    function equirectangularRaw(lambda, phi) {
+      return [lambda, phi];
+    }
+    
+    equirectangularRaw.invert = equirectangularRaw;
+    
+    function equirectangular() {
+      return projection(equirectangularRaw)
+          .scale(152.63);
+    }
+    
+    function conicEquidistantRaw(y0, y1) {
+      var cy0 = cos$1(y0),
+          n = y0 === y1 ? sin$1(y0) : (cy0 - cos$1(y1)) / (y1 - y0),
+          g = cy0 / n + y0;
+    
+      if (abs$1(n) < epsilon$1) return equirectangularRaw;
+    
+      function project(x, y) {
+        var gy = g - y, nx = n * x;
+        return [gy * sin$1(nx), g - gy * cos$1(nx)];
+      }
+    
+      project.invert = function(x, y) {
+        var gy = g - y,
+            l = atan2$1(x, abs$1(gy)) * sign$1(gy);
+        if (gy * n < 0)
+          l -= pi$1 * sign$1(x) * sign$1(gy);
+        return [l / n, g - sign$1(n) * sqrt$2(x * x + gy * gy)];
+      };
+    
+      return project;
+    }
+    
+    function conicEquidistant() {
+      return conicProjection(conicEquidistantRaw)
+          .scale(131.154)
+          .center([0, 13.9389]);
+    }
+    
+    var A1 = 1.340264,
+        A2 = -0.081106,
+        A3 = 0.000893,
+        A4 = 0.003796,
+        M = sqrt$2(3) / 2,
+        iterations = 12;
+    
+    function equalEarthRaw(lambda, phi) {
+      var l = asin$1(M * sin$1(phi)), l2 = l * l, l6 = l2 * l2 * l2;
+      return [
+        lambda * cos$1(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))),
+        l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))
+      ];
+    }
+    
+    equalEarthRaw.invert = function(x, y) {
+      var l = y, l2 = l * l, l6 = l2 * l2 * l2;
+      for (var i = 0, delta, fy, fpy; i < iterations; ++i) {
+        fy = l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) - y;
+        fpy = A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2);
+        l -= delta = fy / fpy, l2 = l * l, l6 = l2 * l2 * l2;
+        if (abs$1(delta) < epsilon2) break;
+      }
+      return [
+        M * x * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2)) / cos$1(l),
+        asin$1(sin$1(l) / M)
+      ];
+    };
+    
+    function equalEarth() {
+      return projection(equalEarthRaw)
+          .scale(177.158);
+    }
+    
+    function gnomonicRaw(x, y) {
+      var cy = cos$1(y), k = cos$1(x) * cy;
+      return [cy * sin$1(x) / k, sin$1(y) / k];
+    }
+    
+    gnomonicRaw.invert = azimuthalInvert(atan);
+    
+    function gnomonic() {
+      return projection(gnomonicRaw)
+          .scale(144.049)
+          .clipAngle(60);
+    }
+    
+    function identity$4() {
+      var k = 1, tx = 0, ty = 0, sx = 1, sy = 1, // scale, translate and reflect
+          alpha = 0, ca, sa, // angle
+          x0 = null, y0, x1, y1, // clip extent
+          kx = 1, ky = 1,
+          transform = transformer$3({
+            point: function(x, y) {
+              var p = projection([x, y]);
+              this.stream.point(p[0], p[1]);
+            }
+          }),
+          postclip = identity$5,
+          cache,
+          cacheStream;
+    
+      function reset() {
+        kx = k * sx;
+        ky = k * sy;
+        cache = cacheStream = null;
+        return projection;
+      }
+    
+      function projection (p) {
+        var x = p[0] * kx, y = p[1] * ky;
+        if (alpha) {
+          var t = y * ca - x * sa;
+          x = x * ca + y * sa;
+          y = t;
+        }    
+        return [x + tx, y + ty];
+      }
+      projection.invert = function(p) {
+        var x = p[0] - tx, y = p[1] - ty;
+        if (alpha) {
+          var t = y * ca + x * sa;
+          x = x * ca - y * sa;
+          y = t;
+        }
+        return [x / kx, y / ky];
+      };
+      projection.stream = function(stream) {
+        return cache && cacheStream === stream ? cache : cache = transform(postclip(cacheStream = stream));
+      };
+      projection.postclip = function(_) {
+        return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;
+      };
+      projection.clipExtent = function(_) {
+        return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity$5) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];
+      };
+      projection.scale = function(_) {
+        return arguments.length ? (k = +_, reset()) : k;
+      };
+      projection.translate = function(_) {
+        return arguments.length ? (tx = +_[0], ty = +_[1], reset()) : [tx, ty];
+      };
+      projection.angle = function(_) {
+        return arguments.length ? (alpha = _ % 360 * radians, sa = sin$1(alpha), ca = cos$1(alpha), reset()) : alpha * degrees;
+      };
+      projection.reflectX = function(_) {
+        return arguments.length ? (sx = _ ? -1 : 1, reset()) : sx < 0;
+      };
+      projection.reflectY = function(_) {
+        return arguments.length ? (sy = _ ? -1 : 1, reset()) : sy < 0;
+      };
+      projection.fitExtent = function(extent, object) {
+        return fitExtent(projection, extent, object);
+      };
+      projection.fitSize = function(size, object) {
+        return fitSize(projection, size, object);
+      };
+      projection.fitWidth = function(width, object) {
+        return fitWidth(projection, width, object);
+      };
+      projection.fitHeight = function(height, object) {
+        return fitHeight(projection, height, object);
+      };
+    
+      return projection;
+    }
+    
+    function naturalEarth1Raw(lambda, phi) {
+      var phi2 = phi * phi, phi4 = phi2 * phi2;
+      return [
+        lambda * (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))),
+        phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4)))
+      ];
+    }
+    
+    naturalEarth1Raw.invert = function(x, y) {
+      var phi = y, i = 25, delta;
+      do {
+        var phi2 = phi * phi, phi4 = phi2 * phi2;
+        phi -= delta = (phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) - y) /
+            (1.007226 + phi2 * (0.015085 * 3 + phi4 * (-0.044475 * 7 + 0.028874 * 9 * phi2 - 0.005916 * 11 * phi4)));
+      } while (abs$1(delta) > epsilon$1 && --i > 0);
+      return [
+        x / (0.8707 + (phi2 = phi * phi) * (-0.131979 + phi2 * (-0.013791 + phi2 * phi2 * phi2 * (0.003971 - 0.001529 * phi2)))),
+        phi
+      ];
+    };
+    
+    function naturalEarth1() {
+      return projection(naturalEarth1Raw)
+          .scale(175.295);
+    }
+    
+    function orthographicRaw(x, y) {
+      return [cos$1(y) * sin$1(x), sin$1(y)];
+    }
+    
+    orthographicRaw.invert = azimuthalInvert(asin$1);
+    
+    function orthographic() {
+      return projection(orthographicRaw)
+          .scale(249.5)
+          .clipAngle(90 + epsilon$1);
+    }
+    
+    function stereographicRaw(x, y) {
+      var cy = cos$1(y), k = 1 + cos$1(x) * cy;
+      return [cy * sin$1(x) / k, sin$1(y) / k];
+    }
+    
+    stereographicRaw.invert = azimuthalInvert(function(z) {
+      return 2 * atan(z);
+    });
+    
+    function stereographic() {
+      return projection(stereographicRaw)
+          .scale(250)
+          .clipAngle(142);
+    }
+    
+    function transverseMercatorRaw(lambda, phi) {
+      return [log$1(tan((halfPi$1 + phi) / 2)), -lambda];
+    }
+    
+    transverseMercatorRaw.invert = function(x, y) {
+      return [-y, 2 * atan(exp(x)) - halfPi$1];
+    };
+    
+    function transverseMercator() {
+      var m = mercatorProjection(transverseMercatorRaw),
+          center = m.center,
+          rotate = m.rotate;
+    
+      m.center = function(_) {
+        return arguments.length ? center([-_[1], _[0]]) : (_ = center(), [_[1], -_[0]]);
+      };
+    
+      m.rotate = function(_) {
+        return arguments.length ? rotate([_[0], _[1], _.length > 2 ? _[2] + 90 : 90]) : (_ = rotate(), [_[0], _[1], _[2] - 90]);
+      };
+    
+      return rotate([0, 0, 90])
+          .scale(159.155);
+    }
+    
+    function defaultSeparation$1(a, b) {
+      return a.parent === b.parent ? 1 : 2;
+    }
+    
+    function meanX(children) {
+      return children.reduce(meanXReduce, 0) / children.length;
+    }
+    
+    function meanXReduce(x, c) {
+      return x + c.x;
+    }
+    
+    function maxY(children) {
+      return 1 + children.reduce(maxYReduce, 0);
+    }
+    
+    function maxYReduce(y, c) {
+      return Math.max(y, c.y);
+    }
+    
+    function leafLeft(node) {
+      var children;
+      while (children = node.children) node = children[0];
+      return node;
+    }
+    
+    function leafRight(node) {
+      var children;
+      while (children = node.children) node = children[children.length - 1];
+      return node;
+    }
+    
+    function cluster() {
+      var separation = defaultSeparation$1,
+          dx = 1,
+          dy = 1,
+          nodeSize = false;
+    
+      function cluster(root) {
+        var previousNode,
+            x = 0;
+    
+        // First walk, computing the initial x & y values.
+        root.eachAfter(function(node) {
+          var children = node.children;
+          if (children) {
+            node.x = meanX(children);
+            node.y = maxY(children);
+          } else {
+            node.x = previousNode ? x += separation(node, previousNode) : 0;
+            node.y = 0;
+            previousNode = node;
+          }
+        });
+    
+        var left = leafLeft(root),
+            right = leafRight(root),
+            x0 = left.x - separation(left, right) / 2,
+            x1 = right.x + separation(right, left) / 2;
+    
+        // Second walk, normalizing x & y to the desired size.
+        return root.eachAfter(nodeSize ? function(node) {
+          node.x = (node.x - root.x) * dx;
+          node.y = (root.y - node.y) * dy;
+        } : function(node) {
+          node.x = (node.x - x0) / (x1 - x0) * dx;
+          node.y = (1 - (root.y ? node.y / root.y : 1)) * dy;
+        });
+      }
+    
+      cluster.separation = function(x) {
+        return arguments.length ? (separation = x, cluster) : separation;
+      };
+    
+      cluster.size = function(x) {
+        return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? null : [dx, dy]);
+      };
+    
+      cluster.nodeSize = function(x) {
+        return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? [dx, dy] : null);
+      };
+    
+      return cluster;
+    }
+    
+    function count(node) {
+      var sum = 0,
+          children = node.children,
+          i = children && children.length;
+      if (!i) sum = 1;
+      else while (--i >= 0) sum += children[i].value;
+      node.value = sum;
+    }
+    
+    function node_count() {
+      return this.eachAfter(count);
+    }
+    
+    function node_each(callback, that) {
+      let index = -1;
+      for (const node of this) {
+        callback.call(that, node, ++index, this);
+      }
+      return this;
+    }
+    
+    function node_eachBefore(callback, that) {
+      var node = this, nodes = [node], children, i, index = -1;
+      while (node = nodes.pop()) {
+        callback.call(that, node, ++index, this);
+        if (children = node.children) {
+          for (i = children.length - 1; i >= 0; --i) {
+            nodes.push(children[i]);
+          }
+        }
+      }
+      return this;
+    }
+    
+    function node_eachAfter(callback, that) {
+      var node = this, nodes = [node], next = [], children, i, n, index = -1;
+      while (node = nodes.pop()) {
+        next.push(node);
+        if (children = node.children) {
+          for (i = 0, n = children.length; i < n; ++i) {
+            nodes.push(children[i]);
+          }
+        }
+      }
+      while (node = next.pop()) {
+        callback.call(that, node, ++index, this);
+      }
+      return this;
+    }
+    
+    function node_find(callback, that) {
+      let index = -1;
+      for (const node of this) {
+        if (callback.call(that, node, ++index, this)) {
+          return node;
+        }
+      }
+    }
+    
+    function node_sum(value) {
+      return this.eachAfter(function(node) {
+        var sum = +value(node.data) || 0,
+            children = node.children,
+            i = children && children.length;
+        while (--i >= 0) sum += children[i].value;
+        node.value = sum;
+      });
+    }
+    
+    function node_sort(compare) {
+      return this.eachBefore(function(node) {
+        if (node.children) {
+          node.children.sort(compare);
+        }
+      });
+    }
+    
+    function node_path(end) {
+      var start = this,
+          ancestor = leastCommonAncestor(start, end),
+          nodes = [start];
+      while (start !== ancestor) {
+        start = start.parent;
+        nodes.push(start);
+      }
+      var k = nodes.length;
+      while (end !== ancestor) {
+        nodes.splice(k, 0, end);
+        end = end.parent;
+      }
+      return nodes;
+    }
+    
+    function leastCommonAncestor(a, b) {
+      if (a === b) return a;
+      var aNodes = a.ancestors(),
+          bNodes = b.ancestors(),
+          c = null;
+      a = aNodes.pop();
+      b = bNodes.pop();
+      while (a === b) {
+        c = a;
+        a = aNodes.pop();
+        b = bNodes.pop();
+      }
+      return c;
+    }
+    
+    function node_ancestors() {
+      var node = this, nodes = [node];
+      while (node = node.parent) {
+        nodes.push(node);
+      }
+      return nodes;
+    }
+    
+    function node_descendants() {
+      return Array.from(this);
+    }
+    
+    function node_leaves() {
+      var leaves = [];
+      this.eachBefore(function(node) {
+        if (!node.children) {
+          leaves.push(node);
+        }
+      });
+      return leaves;
+    }
+    
+    function node_links() {
+      var root = this, links = [];
+      root.each(function(node) {
+        if (node !== root) { // Don’t include the root’s parent, if any.
+          links.push({source: node.parent, target: node});
+        }
+      });
+      return links;
+    }
+    
+    function* node_iterator() {
+      var node = this, current, next = [node], children, i, n;
+      do {
+        current = next.reverse(), next = [];
+        while (node = current.pop()) {
+          yield node;
+          if (children = node.children) {
+            for (i = 0, n = children.length; i < n; ++i) {
+              next.push(children[i]);
+            }
+          }
+        }
+      } while (next.length);
+    }
+    
+    function hierarchy(data, children) {
+      if (data instanceof Map) {
+        data = [undefined, data];
+        if (children === undefined) children = mapChildren;
+      } else if (children === undefined) {
+        children = objectChildren;
+      }
+    
+      var root = new Node$1(data),
+          node,
+          nodes = [root],
+          child,
+          childs,
+          i,
+          n;
+    
+      while (node = nodes.pop()) {
+        if ((childs = children(node.data)) && (n = (childs = Array.from(childs)).length)) {
+          node.children = childs;
+          for (i = n - 1; i >= 0; --i) {
+            nodes.push(child = childs[i] = new Node$1(childs[i]));
+            child.parent = node;
+            child.depth = node.depth + 1;
+          }
+        }
+      }
+    
+      return root.eachBefore(computeHeight);
+    }
+    
+    function node_copy() {
+      return hierarchy(this).eachBefore(copyData);
+    }
+    
+    function objectChildren(d) {
+      return d.children;
+    }
+    
+    function mapChildren(d) {
+      return Array.isArray(d) ? d[1] : null;
+    }
+    
+    function copyData(node) {
+      if (node.data.value !== undefined) node.value = node.data.value;
+      node.data = node.data.data;
+    }
+    
+    function computeHeight(node) {
+      var height = 0;
+      do node.height = height;
+      while ((node = node.parent) && (node.height < ++height));
+    }
+    
+    function Node$1(data) {
+      this.data = data;
+      this.depth =
+      this.height = 0;
+      this.parent = null;
+    }
+    
+    Node$1.prototype = hierarchy.prototype = {
+      constructor: Node$1,
+      count: node_count,
+      each: node_each,
+      eachAfter: node_eachAfter,
+      eachBefore: node_eachBefore,
+      find: node_find,
+      sum: node_sum,
+      sort: node_sort,
+      path: node_path,
+      ancestors: node_ancestors,
+      descendants: node_descendants,
+      leaves: node_leaves,
+      links: node_links,
+      copy: node_copy,
+      [Symbol.iterator]: node_iterator
+    };
+    
+    function array$1(x) {
+      return typeof x === "object" && "length" in x
+        ? x // Array, TypedArray, NodeList, array-like
+        : Array.from(x); // Map, Set, iterable, string, or anything else
+    }
+    
+    function shuffle(array) {
+      var m = array.length,
+          t,
+          i;
+    
+      while (m) {
+        i = Math.random() * m-- | 0;
+        t = array[m];
+        array[m] = array[i];
+        array[i] = t;
+      }
+    
+      return array;
+    }
+    
+    function enclose(circles) {
+      var i = 0, n = (circles = shuffle(Array.from(circles))).length, B = [], p, e;
+    
+      while (i < n) {
+        p = circles[i];
+        if (e && enclosesWeak(e, p)) ++i;
+        else e = encloseBasis(B = extendBasis(B, p)), i = 0;
+      }
+    
+      return e;
+    }
+    
+    function extendBasis(B, p) {
+      var i, j;
+    
+      if (enclosesWeakAll(p, B)) return [p];
+    
+      // If we get here then B must have at least one element.
+      for (i = 0; i < B.length; ++i) {
+        if (enclosesNot(p, B[i])
+            && enclosesWeakAll(encloseBasis2(B[i], p), B)) {
+          return [B[i], p];
+        }
+      }
+    
+      // If we get here then B must have at least two elements.
+      for (i = 0; i < B.length - 1; ++i) {
+        for (j = i + 1; j < B.length; ++j) {
+          if (enclosesNot(encloseBasis2(B[i], B[j]), p)
+              && enclosesNot(encloseBasis2(B[i], p), B[j])
+              && enclosesNot(encloseBasis2(B[j], p), B[i])
+              && enclosesWeakAll(encloseBasis3(B[i], B[j], p), B)) {
+            return [B[i], B[j], p];
+          }
+        }
+      }
+    
+      // If we get here then something is very wrong.
+      throw new Error;
+    }
+    
+    function enclosesNot(a, b) {
+      var dr = a.r - b.r, dx = b.x - a.x, dy = b.y - a.y;
+      return dr < 0 || dr * dr < dx * dx + dy * dy;
+    }
+    
+    function enclosesWeak(a, b) {
+      var dr = a.r - b.r + Math.max(a.r, b.r, 1) * 1e-9, dx = b.x - a.x, dy = b.y - a.y;
+      return dr > 0 && dr * dr > dx * dx + dy * dy;
+    }
+    
+    function enclosesWeakAll(a, B) {
+      for (var i = 0; i < B.length; ++i) {
+        if (!enclosesWeak(a, B[i])) {
+          return false;
+        }
+      }
+      return true;
+    }
+    
+    function encloseBasis(B) {
+      switch (B.length) {
+        case 1: return encloseBasis1(B[0]);
+        case 2: return encloseBasis2(B[0], B[1]);
+        case 3: return encloseBasis3(B[0], B[1], B[2]);
+      }
+    }
+    
+    function encloseBasis1(a) {
+      return {
+        x: a.x,
+        y: a.y,
+        r: a.r
+      };
+    }
+    
+    function encloseBasis2(a, b) {
+      var x1 = a.x, y1 = a.y, r1 = a.r,
+          x2 = b.x, y2 = b.y, r2 = b.r,
+          x21 = x2 - x1, y21 = y2 - y1, r21 = r2 - r1,
+          l = Math.sqrt(x21 * x21 + y21 * y21);
+      return {
+        x: (x1 + x2 + x21 / l * r21) / 2,
+        y: (y1 + y2 + y21 / l * r21) / 2,
+        r: (l + r1 + r2) / 2
+      };
+    }
+    
+    function encloseBasis3(a, b, c) {
+      var x1 = a.x, y1 = a.y, r1 = a.r,
+          x2 = b.x, y2 = b.y, r2 = b.r,
+          x3 = c.x, y3 = c.y, r3 = c.r,
+          a2 = x1 - x2,
+          a3 = x1 - x3,
+          b2 = y1 - y2,
+          b3 = y1 - y3,
+          c2 = r2 - r1,
+          c3 = r3 - r1,
+          d1 = x1 * x1 + y1 * y1 - r1 * r1,
+          d2 = d1 - x2 * x2 - y2 * y2 + r2 * r2,
+          d3 = d1 - x3 * x3 - y3 * y3 + r3 * r3,
+          ab = a3 * b2 - a2 * b3,
+          xa = (b2 * d3 - b3 * d2) / (ab * 2) - x1,
+          xb = (b3 * c2 - b2 * c3) / ab,
+          ya = (a3 * d2 - a2 * d3) / (ab * 2) - y1,
+          yb = (a2 * c3 - a3 * c2) / ab,
+          A = xb * xb + yb * yb - 1,
+          B = 2 * (r1 + xa * xb + ya * yb),
+          C = xa * xa + ya * ya - r1 * r1,
+          r = -(A ? (B + Math.sqrt(B * B - 4 * A * C)) / (2 * A) : C / B);
+      return {
+        x: x1 + xa + xb * r,
+        y: y1 + ya + yb * r,
+        r: r
+      };
+    }
+    
+    function place(b, a, c) {
+      var dx = b.x - a.x, x, a2,
+          dy = b.y - a.y, y, b2,
+          d2 = dx * dx + dy * dy;
+      if (d2) {
+        a2 = a.r + c.r, a2 *= a2;
+        b2 = b.r + c.r, b2 *= b2;
+        if (a2 > b2) {
+          x = (d2 + b2 - a2) / (2 * d2);
+          y = Math.sqrt(Math.max(0, b2 / d2 - x * x));
+          c.x = b.x - x * dx - y * dy;
+          c.y = b.y - x * dy + y * dx;
+        } else {
+          x = (d2 + a2 - b2) / (2 * d2);
+          y = Math.sqrt(Math.max(0, a2 / d2 - x * x));
+          c.x = a.x + x * dx - y * dy;
+          c.y = a.y + x * dy + y * dx;
+        }
+      } else {
+        c.x = a.x + c.r;
+        c.y = a.y;
+      }
+    }
+    
+    function intersects(a, b) {
+      var dr = a.r + b.r - 1e-6, dx = b.x - a.x, dy = b.y - a.y;
+      return dr > 0 && dr * dr > dx * dx + dy * dy;
+    }
+    
+    function score(node) {
+      var a = node._,
+          b = node.next._,
+          ab = a.r + b.r,
+          dx = (a.x * b.r + b.x * a.r) / ab,
+          dy = (a.y * b.r + b.y * a.r) / ab;
+      return dx * dx + dy * dy;
+    }
+    
+    function Node(circle) {
+      this._ = circle;
+      this.next = null;
+      this.previous = null;
+    }
+    
+    function packEnclose(circles) {
+      if (!(n = (circles = array$1(circles)).length)) return 0;
+    
+      var a, b, c, n, aa, ca, i, j, k, sj, sk;
+    
+      // Place the first circle.
+      a = circles[0], a.x = 0, a.y = 0;
+      if (!(n > 1)) return a.r;
+    
+      // Place the second circle.
+      b = circles[1], a.x = -b.r, b.x = a.r, b.y = 0;
+      if (!(n > 2)) return a.r + b.r;
+    
+      // Place the third circle.
+      place(b, a, c = circles[2]);
+    
+      // Initialize the front-chain using the first three circles a, b and c.
+      a = new Node(a), b = new Node(b), c = new Node(c);
+      a.next = c.previous = b;
+      b.next = a.previous = c;
+      c.next = b.previous = a;
+    
+      // Attempt to place each remaining circle…
+      pack: for (i = 3; i < n; ++i) {
+        place(a._, b._, c = circles[i]), c = new Node(c);
+    
+        // Find the closest intersecting circle on the front-chain, if any.
+        // “Closeness” is determined by linear distance along the front-chain.
+        // “Ahead” or “behind” is likewise determined by linear distance.
+        j = b.next, k = a.previous, sj = b._.r, sk = a._.r;
+        do {
+          if (sj <= sk) {
+            if (intersects(j._, c._)) {
+              b = j, a.next = b, b.previous = a, --i;
+              continue pack;
+            }
+            sj += j._.r, j = j.next;
+          } else {
+            if (intersects(k._, c._)) {
+              a = k, a.next = b, b.previous = a, --i;
+              continue pack;
+            }
+            sk += k._.r, k = k.previous;
+          }
+        } while (j !== k.next);
+    
+        // Success! Insert the new circle c between a and b.
+        c.previous = a, c.next = b, a.next = b.previous = b = c;
+    
+        // Compute the new closest circle pair to the centroid.
+        aa = score(a);
+        while ((c = c.next) !== b) {
+          if ((ca = score(c)) < aa) {
+            a = c, aa = ca;
+          }
+        }
+        b = a.next;
+      }
+    
+      // Compute the enclosing circle of the front chain.
+      a = [b._], c = b; while ((c = c.next) !== b) a.push(c._); c = enclose(a);
+    
+      // Translate the circles to put the enclosing circle around the origin.
+      for (i = 0; i < n; ++i) a = circles[i], a.x -= c.x, a.y -= c.y;
+    
+      return c.r;
+    }
+    
+    function siblings(circles) {
+      packEnclose(circles);
+      return circles;
+    }
+    
+    function optional(f) {
+      return f == null ? null : required(f);
+    }
+    
+    function required(f) {
+      if (typeof f !== "function") throw new Error;
+      return f;
+    }
+    
+    function constantZero() {
+      return 0;
+    }
+    
+    function constant$2(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function defaultRadius(d) {
+      return Math.sqrt(d.value);
+    }
+    
+    function index$1() {
+      var radius = null,
+          dx = 1,
+          dy = 1,
+          padding = constantZero;
+    
+      function pack(root) {
+        root.x = dx / 2, root.y = dy / 2;
+        if (radius) {
+          root.eachBefore(radiusLeaf(radius))
+              .eachAfter(packChildren(padding, 0.5))
+              .eachBefore(translateChild(1));
+        } else {
+          root.eachBefore(radiusLeaf(defaultRadius))
+              .eachAfter(packChildren(constantZero, 1))
+              .eachAfter(packChildren(padding, root.r / Math.min(dx, dy)))
+              .eachBefore(translateChild(Math.min(dx, dy) / (2 * root.r)));
+        }
+        return root;
+      }
+    
+      pack.radius = function(x) {
+        return arguments.length ? (radius = optional(x), pack) : radius;
+      };
+    
+      pack.size = function(x) {
+        return arguments.length ? (dx = +x[0], dy = +x[1], pack) : [dx, dy];
+      };
+    
+      pack.padding = function(x) {
+        return arguments.length ? (padding = typeof x === "function" ? x : constant$2(+x), pack) : padding;
+      };
+    
+      return pack;
+    }
+    
+    function radiusLeaf(radius) {
+      return function(node) {
+        if (!node.children) {
+          node.r = Math.max(0, +radius(node) || 0);
+        }
+      };
+    }
+    
+    function packChildren(padding, k) {
+      return function(node) {
+        if (children = node.children) {
+          var children,
+              i,
+              n = children.length,
+              r = padding(node) * k || 0,
+              e;
+    
+          if (r) for (i = 0; i < n; ++i) children[i].r += r;
+          e = packEnclose(children);
+          if (r) for (i = 0; i < n; ++i) children[i].r -= r;
+          node.r = e + r;
+        }
+      };
+    }
+    
+    function translateChild(k) {
+      return function(node) {
+        var parent = node.parent;
+        node.r *= k;
+        if (parent) {
+          node.x = parent.x + k * node.x;
+          node.y = parent.y + k * node.y;
+        }
+      };
+    }
+    
+    function roundNode(node) {
+      node.x0 = Math.round(node.x0);
+      node.y0 = Math.round(node.y0);
+      node.x1 = Math.round(node.x1);
+      node.y1 = Math.round(node.y1);
+    }
+    
+    function treemapDice(parent, x0, y0, x1, y1) {
+      var nodes = parent.children,
+          node,
+          i = -1,
+          n = nodes.length,
+          k = parent.value && (x1 - x0) / parent.value;
+    
+      while (++i < n) {
+        node = nodes[i], node.y0 = y0, node.y1 = y1;
+        node.x0 = x0, node.x1 = x0 += node.value * k;
+      }
+    }
+    
+    function partition() {
+      var dx = 1,
+          dy = 1,
+          padding = 0,
+          round = false;
+    
+      function partition(root) {
+        var n = root.height + 1;
+        root.x0 =
+        root.y0 = padding;
+        root.x1 = dx;
+        root.y1 = dy / n;
+        root.eachBefore(positionNode(dy, n));
+        if (round) root.eachBefore(roundNode);
+        return root;
+      }
+    
+      function positionNode(dy, n) {
+        return function(node) {
+          if (node.children) {
+            treemapDice(node, node.x0, dy * (node.depth + 1) / n, node.x1, dy * (node.depth + 2) / n);
+          }
+          var x0 = node.x0,
+              y0 = node.y0,
+              x1 = node.x1 - padding,
+              y1 = node.y1 - padding;
+          if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+          if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+          node.x0 = x0;
+          node.y0 = y0;
+          node.x1 = x1;
+          node.y1 = y1;
+        };
+      }
+    
+      partition.round = function(x) {
+        return arguments.length ? (round = !!x, partition) : round;
+      };
+    
+      partition.size = function(x) {
+        return arguments.length ? (dx = +x[0], dy = +x[1], partition) : [dx, dy];
+      };
+    
+      partition.padding = function(x) {
+        return arguments.length ? (padding = +x, partition) : padding;
+      };
+    
+      return partition;
+    }
+    
+    var preroot = {depth: -1},
+        ambiguous = {};
+    
+    function defaultId(d) {
+      return d.id;
+    }
+    
+    function defaultParentId(d) {
+      return d.parentId;
+    }
+    
+    function stratify() {
+      var id = defaultId,
+          parentId = defaultParentId;
+    
+      function stratify(data) {
+        var nodes = Array.from(data),
+            n = nodes.length,
+            d,
+            i,
+            root,
+            parent,
+            node,
+            nodeId,
+            nodeKey,
+            nodeByKey = new Map;
+    
+        for (i = 0; i < n; ++i) {
+          d = nodes[i], node = nodes[i] = new Node$1(d);
+          if ((nodeId = id(d, i, data)) != null && (nodeId += "")) {
+            nodeKey = node.id = nodeId;
+            nodeByKey.set(nodeKey, nodeByKey.has(nodeKey) ? ambiguous : node);
+          }
+          if ((nodeId = parentId(d, i, data)) != null && (nodeId += "")) {
+            node.parent = nodeId;
+          }
+        }
+    
+        for (i = 0; i < n; ++i) {
+          node = nodes[i];
+          if (nodeId = node.parent) {
+            parent = nodeByKey.get(nodeId);
+            if (!parent) throw new Error("missing: " + nodeId);
+            if (parent === ambiguous) throw new Error("ambiguous: " + nodeId);
+            if (parent.children) parent.children.push(node);
+            else parent.children = [node];
+            node.parent = parent;
+          } else {
+            if (root) throw new Error("multiple roots");
+            root = node;
+          }
+        }
+    
+        if (!root) throw new Error("no root");
+        root.parent = preroot;
+        root.eachBefore(function(node) { node.depth = node.parent.depth + 1; --n; }).eachBefore(computeHeight);
+        root.parent = null;
+        if (n > 0) throw new Error("cycle");
+    
+        return root;
+      }
+    
+      stratify.id = function(x) {
+        return arguments.length ? (id = required(x), stratify) : id;
+      };
+    
+      stratify.parentId = function(x) {
+        return arguments.length ? (parentId = required(x), stratify) : parentId;
+      };
+    
+      return stratify;
+    }
+    
+    function defaultSeparation(a, b) {
+      return a.parent === b.parent ? 1 : 2;
+    }
+    
+    // function radialSeparation(a, b) {
+    //   return (a.parent === b.parent ? 1 : 2) / a.depth;
+    // }
+    
+    // This function is used to traverse the left contour of a subtree (or
+    // subforest). It returns the successor of v on this contour. This successor is
+    // either given by the leftmost child of v or by the thread of v. The function
+    // returns null if and only if v is on the highest level of its subtree.
+    function nextLeft(v) {
+      var children = v.children;
+      return children ? children[0] : v.t;
+    }
+    
+    // This function works analogously to nextLeft.
+    function nextRight(v) {
+      var children = v.children;
+      return children ? children[children.length - 1] : v.t;
+    }
+    
+    // Shifts the current subtree rooted at w+. This is done by increasing
+    // prelim(w+) and mod(w+) by shift.
+    function moveSubtree(wm, wp, shift) {
+      var change = shift / (wp.i - wm.i);
+      wp.c -= change;
+      wp.s += shift;
+      wm.c += change;
+      wp.z += shift;
+      wp.m += shift;
+    }
+    
+    // All other shifts, applied to the smaller subtrees between w- and w+, are
+    // performed by this function. To prepare the shifts, we have to adjust
+    // change(w+), shift(w+), and change(w-).
+    function executeShifts(v) {
+      var shift = 0,
+          change = 0,
+          children = v.children,
+          i = children.length,
+          w;
+      while (--i >= 0) {
+        w = children[i];
+        w.z += shift;
+        w.m += shift;
+        shift += w.s + (change += w.c);
+      }
+    }
+    
+    // If vi-’s ancestor is a sibling of v, returns vi-’s ancestor. Otherwise,
+    // returns the specified (default) ancestor.
+    function nextAncestor(vim, v, ancestor) {
+      return vim.a.parent === v.parent ? vim.a : ancestor;
+    }
+    
+    function TreeNode(node, i) {
+      this._ = node;
+      this.parent = null;
+      this.children = null;
+      this.A = null; // default ancestor
+      this.a = this; // ancestor
+      this.z = 0; // prelim
+      this.m = 0; // mod
+      this.c = 0; // change
+      this.s = 0; // shift
+      this.t = null; // thread
+      this.i = i; // number
+    }
+    
+    TreeNode.prototype = Object.create(Node$1.prototype);
+    
+    function treeRoot(root) {
+      var tree = new TreeNode(root, 0),
+          node,
+          nodes = [tree],
+          child,
+          children,
+          i,
+          n;
+    
+      while (node = nodes.pop()) {
+        if (children = node._.children) {
+          node.children = new Array(n = children.length);
+          for (i = n - 1; i >= 0; --i) {
+            nodes.push(child = node.children[i] = new TreeNode(children[i], i));
+            child.parent = node;
+          }
+        }
+      }
+    
+      (tree.parent = new TreeNode(null, 0)).children = [tree];
+      return tree;
+    }
+    
+    // Node-link tree diagram using the Reingold-Tilford "tidy" algorithm
+    function tree() {
+      var separation = defaultSeparation,
+          dx = 1,
+          dy = 1,
+          nodeSize = null;
+    
+      function tree(root) {
+        var t = treeRoot(root);
+    
+        // Compute the layout using Buchheim et al.’s algorithm.
+        t.eachAfter(firstWalk), t.parent.m = -t.z;
+        t.eachBefore(secondWalk);
+    
+        // If a fixed node size is specified, scale x and y.
+        if (nodeSize) root.eachBefore(sizeNode);
+    
+        // If a fixed tree size is specified, scale x and y based on the extent.
+        // Compute the left-most, right-most, and depth-most nodes for extents.
+        else {
+          var left = root,
+              right = root,
+              bottom = root;
+          root.eachBefore(function(node) {
+            if (node.x < left.x) left = node;
+            if (node.x > right.x) right = node;
+            if (node.depth > bottom.depth) bottom = node;
+          });
+          var s = left === right ? 1 : separation(left, right) / 2,
+              tx = s - left.x,
+              kx = dx / (right.x + s + tx),
+              ky = dy / (bottom.depth || 1);
+          root.eachBefore(function(node) {
+            node.x = (node.x + tx) * kx;
+            node.y = node.depth * ky;
+          });
+        }
+    
+        return root;
+      }
+    
+      // Computes a preliminary x-coordinate for v. Before that, FIRST WALK is
+      // applied recursively to the children of v, as well as the function
+      // APPORTION. After spacing out the children by calling EXECUTE SHIFTS, the
+      // node v is placed to the midpoint of its outermost children.
+      function firstWalk(v) {
+        var children = v.children,
+            siblings = v.parent.children,
+            w = v.i ? siblings[v.i - 1] : null;
+        if (children) {
+          executeShifts(v);
+          var midpoint = (children[0].z + children[children.length - 1].z) / 2;
+          if (w) {
+            v.z = w.z + separation(v._, w._);
+            v.m = v.z - midpoint;
+          } else {
+            v.z = midpoint;
+          }
+        } else if (w) {
+          v.z = w.z + separation(v._, w._);
+        }
+        v.parent.A = apportion(v, w, v.parent.A || siblings[0]);
+      }
+    
+      // Computes all real x-coordinates by summing up the modifiers recursively.
+      function secondWalk(v) {
+        v._.x = v.z + v.parent.m;
+        v.m += v.parent.m;
+      }
+    
+      // The core of the algorithm. Here, a new subtree is combined with the
+      // previous subtrees. Threads are used to traverse the inside and outside
+      // contours of the left and right subtree up to the highest common level. The
+      // vertices used for the traversals are vi+, vi-, vo-, and vo+, where the
+      // superscript o means outside and i means inside, the subscript - means left
+      // subtree and + means right subtree. For summing up the modifiers along the
+      // contour, we use respective variables si+, si-, so-, and so+. Whenever two
+      // nodes of the inside contours conflict, we compute the left one of the
+      // greatest uncommon ancestors using the function ANCESTOR and call MOVE
+      // SUBTREE to shift the subtree and prepare the shifts of smaller subtrees.
+      // Finally, we add a new thread (if necessary).
+      function apportion(v, w, ancestor) {
+        if (w) {
+          var vip = v,
+              vop = v,
+              vim = w,
+              vom = vip.parent.children[0],
+              sip = vip.m,
+              sop = vop.m,
+              sim = vim.m,
+              som = vom.m,
+              shift;
+          while (vim = nextRight(vim), vip = nextLeft(vip), vim && vip) {
+            vom = nextLeft(vom);
+            vop = nextRight(vop);
+            vop.a = v;
+            shift = vim.z + sim - vip.z - sip + separation(vim._, vip._);
+            if (shift > 0) {
+              moveSubtree(nextAncestor(vim, v, ancestor), v, shift);
+              sip += shift;
+              sop += shift;
+            }
+            sim += vim.m;
+            sip += vip.m;
+            som += vom.m;
+            sop += vop.m;
+          }
+          if (vim && !nextRight(vop)) {
+            vop.t = vim;
+            vop.m += sim - sop;
+          }
+          if (vip && !nextLeft(vom)) {
+            vom.t = vip;
+            vom.m += sip - som;
+            ancestor = v;
+          }
+        }
+        return ancestor;
+      }
+    
+      function sizeNode(node) {
+        node.x *= dx;
+        node.y = node.depth * dy;
+      }
+    
+      tree.separation = function(x) {
+        return arguments.length ? (separation = x, tree) : separation;
+      };
+    
+      tree.size = function(x) {
+        return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], tree) : (nodeSize ? null : [dx, dy]);
+      };
+    
+      tree.nodeSize = function(x) {
+        return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], tree) : (nodeSize ? [dx, dy] : null);
+      };
+    
+      return tree;
+    }
+    
+    function treemapSlice(parent, x0, y0, x1, y1) {
+      var nodes = parent.children,
+          node,
+          i = -1,
+          n = nodes.length,
+          k = parent.value && (y1 - y0) / parent.value;
+    
+      while (++i < n) {
+        node = nodes[i], node.x0 = x0, node.x1 = x1;
+        node.y0 = y0, node.y1 = y0 += node.value * k;
+      }
+    }
+    
+    var phi = (1 + Math.sqrt(5)) / 2;
+    
+    function squarifyRatio(ratio, parent, x0, y0, x1, y1) {
+      var rows = [],
+          nodes = parent.children,
+          row,
+          nodeValue,
+          i0 = 0,
+          i1 = 0,
+          n = nodes.length,
+          dx, dy,
+          value = parent.value,
+          sumValue,
+          minValue,
+          maxValue,
+          newRatio,
+          minRatio,
+          alpha,
+          beta;
+    
+      while (i0 < n) {
+        dx = x1 - x0, dy = y1 - y0;
+    
+        // Find the next non-empty node.
+        do sumValue = nodes[i1++].value; while (!sumValue && i1 < n);
+        minValue = maxValue = sumValue;
+        alpha = Math.max(dy / dx, dx / dy) / (value * ratio);
+        beta = sumValue * sumValue * alpha;
+        minRatio = Math.max(maxValue / beta, beta / minValue);
+    
+        // Keep adding nodes while the aspect ratio maintains or improves.
+        for (; i1 < n; ++i1) {
+          sumValue += nodeValue = nodes[i1].value;
+          if (nodeValue < minValue) minValue = nodeValue;
+          if (nodeValue > maxValue) maxValue = nodeValue;
+          beta = sumValue * sumValue * alpha;
+          newRatio = Math.max(maxValue / beta, beta / minValue);
+          if (newRatio > minRatio) { sumValue -= nodeValue; break; }
+          minRatio = newRatio;
+        }
+    
+        // Position and record the row orientation.
+        rows.push(row = {value: sumValue, dice: dx < dy, children: nodes.slice(i0, i1)});
+        if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += dy * sumValue / value : y1);
+        else treemapSlice(row, x0, y0, value ? x0 += dx * sumValue / value : x1, y1);
+        value -= sumValue, i0 = i1;
+      }
+    
+      return rows;
+    }
+    
+    var squarify = (function custom(ratio) {
+    
+      function squarify(parent, x0, y0, x1, y1) {
+        squarifyRatio(ratio, parent, x0, y0, x1, y1);
+      }
+    
+      squarify.ratio = function(x) {
+        return custom((x = +x) > 1 ? x : 1);
+      };
+    
+      return squarify;
+    })(phi);
+    
+    function index() {
+      var tile = squarify,
+          round = false,
+          dx = 1,
+          dy = 1,
+          paddingStack = [0],
+          paddingInner = constantZero,
+          paddingTop = constantZero,
+          paddingRight = constantZero,
+          paddingBottom = constantZero,
+          paddingLeft = constantZero;
+    
+      function treemap(root) {
+        root.x0 =
+        root.y0 = 0;
+        root.x1 = dx;
+        root.y1 = dy;
+        root.eachBefore(positionNode);
+        paddingStack = [0];
+        if (round) root.eachBefore(roundNode);
+        return root;
+      }
+    
+      function positionNode(node) {
+        var p = paddingStack[node.depth],
+            x0 = node.x0 + p,
+            y0 = node.y0 + p,
+            x1 = node.x1 - p,
+            y1 = node.y1 - p;
+        if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+        if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+        node.x0 = x0;
+        node.y0 = y0;
+        node.x1 = x1;
+        node.y1 = y1;
+        if (node.children) {
+          p = paddingStack[node.depth + 1] = paddingInner(node) / 2;
+          x0 += paddingLeft(node) - p;
+          y0 += paddingTop(node) - p;
+          x1 -= paddingRight(node) - p;
+          y1 -= paddingBottom(node) - p;
+          if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
+          if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
+          tile(node, x0, y0, x1, y1);
+        }
+      }
+    
+      treemap.round = function(x) {
+        return arguments.length ? (round = !!x, treemap) : round;
+      };
+    
+      treemap.size = function(x) {
+        return arguments.length ? (dx = +x[0], dy = +x[1], treemap) : [dx, dy];
+      };
+    
+      treemap.tile = function(x) {
+        return arguments.length ? (tile = required(x), treemap) : tile;
+      };
+    
+      treemap.padding = function(x) {
+        return arguments.length ? treemap.paddingInner(x).paddingOuter(x) : treemap.paddingInner();
+      };
+    
+      treemap.paddingInner = function(x) {
+        return arguments.length ? (paddingInner = typeof x === "function" ? x : constant$2(+x), treemap) : paddingInner;
+      };
+    
+      treemap.paddingOuter = function(x) {
+        return arguments.length ? treemap.paddingTop(x).paddingRight(x).paddingBottom(x).paddingLeft(x) : treemap.paddingTop();
+      };
+    
+      treemap.paddingTop = function(x) {
+        return arguments.length ? (paddingTop = typeof x === "function" ? x : constant$2(+x), treemap) : paddingTop;
+      };
+    
+      treemap.paddingRight = function(x) {
+        return arguments.length ? (paddingRight = typeof x === "function" ? x : constant$2(+x), treemap) : paddingRight;
+      };
+    
+      treemap.paddingBottom = function(x) {
+        return arguments.length ? (paddingBottom = typeof x === "function" ? x : constant$2(+x), treemap) : paddingBottom;
+      };
+    
+      treemap.paddingLeft = function(x) {
+        return arguments.length ? (paddingLeft = typeof x === "function" ? x : constant$2(+x), treemap) : paddingLeft;
+      };
+    
+      return treemap;
+    }
+    
+    function binary(parent, x0, y0, x1, y1) {
+      var nodes = parent.children,
+          i, n = nodes.length,
+          sum, sums = new Array(n + 1);
+    
+      for (sums[0] = sum = i = 0; i < n; ++i) {
+        sums[i + 1] = sum += nodes[i].value;
+      }
+    
+      partition(0, n, parent.value, x0, y0, x1, y1);
+    
+      function partition(i, j, value, x0, y0, x1, y1) {
+        if (i >= j - 1) {
+          var node = nodes[i];
+          node.x0 = x0, node.y0 = y0;
+          node.x1 = x1, node.y1 = y1;
+          return;
+        }
+    
+        var valueOffset = sums[i],
+            valueTarget = (value / 2) + valueOffset,
+            k = i + 1,
+            hi = j - 1;
+    
+        while (k < hi) {
+          var mid = k + hi >>> 1;
+          if (sums[mid] < valueTarget) k = mid + 1;
+          else hi = mid;
+        }
+    
+        if ((valueTarget - sums[k - 1]) < (sums[k] - valueTarget) && i + 1 < k) --k;
+    
+        var valueLeft = sums[k] - valueOffset,
+            valueRight = value - valueLeft;
+    
+        if ((x1 - x0) > (y1 - y0)) {
+          var xk = value ? (x0 * valueRight + x1 * valueLeft) / value : x1;
+          partition(i, k, valueLeft, x0, y0, xk, y1);
+          partition(k, j, valueRight, xk, y0, x1, y1);
+        } else {
+          var yk = value ? (y0 * valueRight + y1 * valueLeft) / value : y1;
+          partition(i, k, valueLeft, x0, y0, x1, yk);
+          partition(k, j, valueRight, x0, yk, x1, y1);
+        }
+      }
+    }
+    
+    function sliceDice(parent, x0, y0, x1, y1) {
+      (parent.depth & 1 ? treemapSlice : treemapDice)(parent, x0, y0, x1, y1);
+    }
+    
+    var resquarify = (function custom(ratio) {
+    
+      function resquarify(parent, x0, y0, x1, y1) {
+        if ((rows = parent._squarify) && (rows.ratio === ratio)) {
+          var rows,
+              row,
+              nodes,
+              i,
+              j = -1,
+              n,
+              m = rows.length,
+              value = parent.value;
+    
+          while (++j < m) {
+            row = rows[j], nodes = row.children;
+            for (i = row.value = 0, n = nodes.length; i < n; ++i) row.value += nodes[i].value;
+            if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += (y1 - y0) * row.value / value : y1);
+            else treemapSlice(row, x0, y0, value ? x0 += (x1 - x0) * row.value / value : x1, y1);
+            value -= row.value;
+          }
+        } else {
+          parent._squarify = rows = squarifyRatio(ratio, parent, x0, y0, x1, y1);
+          rows.ratio = ratio;
+        }
+      }
+    
+      resquarify.ratio = function(x) {
+        return custom((x = +x) > 1 ? x : 1);
+      };
+    
+      return resquarify;
+    })(phi);
+    
+    function area$1(polygon) {
+      var i = -1,
+          n = polygon.length,
+          a,
+          b = polygon[n - 1],
+          area = 0;
+    
+      while (++i < n) {
+        a = b;
+        b = polygon[i];
+        area += a[1] * b[0] - a[0] * b[1];
+      }
+    
+      return area / 2;
+    }
+    
+    function centroid(polygon) {
+      var i = -1,
+          n = polygon.length,
+          x = 0,
+          y = 0,
+          a,
+          b = polygon[n - 1],
+          c,
+          k = 0;
+    
+      while (++i < n) {
+        a = b;
+        b = polygon[i];
+        k += c = a[0] * b[1] - b[0] * a[1];
+        x += (a[0] + b[0]) * c;
+        y += (a[1] + b[1]) * c;
+      }
+    
+      return k *= 3, [x / k, y / k];
+    }
+    
+    // Returns the 2D cross product of AB and AC vectors, i.e., the z-component of
+    // the 3D cross product in a quadrant I Cartesian coordinate system (+x is
+    // right, +y is up). Returns a positive value if ABC is counter-clockwise,
+    // negative if clockwise, and zero if the points are collinear.
+    function cross$1(a, b, c) {
+      return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
+    }
+    
+    function lexicographicOrder(a, b) {
+      return a[0] - b[0] || a[1] - b[1];
+    }
+    
+    // Computes the upper convex hull per the monotone chain algorithm.
+    // Assumes points.length >= 3, is sorted by x, unique in y.
+    // Returns an array of indices into points in left-to-right order.
+    function computeUpperHullIndexes(points) {
+      const n = points.length,
+          indexes = [0, 1];
+      let size = 2, i;
+    
+      for (i = 2; i < n; ++i) {
+        while (size > 1 && cross$1(points[indexes[size - 2]], points[indexes[size - 1]], points[i]) <= 0) --size;
+        indexes[size++] = i;
+      }
+    
+      return indexes.slice(0, size); // remove popped points
+    }
+    
+    function hull(points) {
+      if ((n = points.length) < 3) return null;
+    
+      var i,
+          n,
+          sortedPoints = new Array(n),
+          flippedPoints = new Array(n);
+    
+      for (i = 0; i < n; ++i) sortedPoints[i] = [+points[i][0], +points[i][1], i];
+      sortedPoints.sort(lexicographicOrder);
+      for (i = 0; i < n; ++i) flippedPoints[i] = [sortedPoints[i][0], -sortedPoints[i][1]];
+    
+      var upperIndexes = computeUpperHullIndexes(sortedPoints),
+          lowerIndexes = computeUpperHullIndexes(flippedPoints);
+    
+      // Construct the hull polygon, removing possible duplicate endpoints.
+      var skipLeft = lowerIndexes[0] === upperIndexes[0],
+          skipRight = lowerIndexes[lowerIndexes.length - 1] === upperIndexes[upperIndexes.length - 1],
+          hull = [];
+    
+      // Add upper hull in right-to-l order.
+      // Then add lower hull in left-to-right order.
+      for (i = upperIndexes.length - 1; i >= 0; --i) hull.push(points[sortedPoints[upperIndexes[i]][2]]);
+      for (i = +skipLeft; i < lowerIndexes.length - skipRight; ++i) hull.push(points[sortedPoints[lowerIndexes[i]][2]]);
+    
+      return hull;
+    }
+    
+    function contains(polygon, point) {
+      var n = polygon.length,
+          p = polygon[n - 1],
+          x = point[0], y = point[1],
+          x0 = p[0], y0 = p[1],
+          x1, y1,
+          inside = false;
+    
+      for (var i = 0; i < n; ++i) {
+        p = polygon[i], x1 = p[0], y1 = p[1];
+        if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside;
+        x0 = x1, y0 = y1;
+      }
+    
+      return inside;
+    }
+    
+    function length(polygon) {
+      var i = -1,
+          n = polygon.length,
+          b = polygon[n - 1],
+          xa,
+          ya,
+          xb = b[0],
+          yb = b[1],
+          perimeter = 0;
+    
+      while (++i < n) {
+        xa = xb;
+        ya = yb;
+        b = polygon[i];
+        xb = b[0];
+        yb = b[1];
+        xa -= xb;
+        ya -= yb;
+        perimeter += Math.hypot(xa, ya);
+      }
+    
+      return perimeter;
+    }
+    
+    var defaultSource = Math.random;
+    
+    var uniform = (function sourceRandomUniform(source) {
+      function randomUniform(min, max) {
+        min = min == null ? 0 : +min;
+        max = max == null ? 1 : +max;
+        if (arguments.length === 1) max = min, min = 0;
+        else max -= min;
+        return function() {
+          return source() * max + min;
+        };
+      }
+    
+      randomUniform.source = sourceRandomUniform;
+    
+      return randomUniform;
+    })(defaultSource);
+    
+    var int = (function sourceRandomInt(source) {
+      function randomInt(min, max) {
+        if (arguments.length < 2) max = min, min = 0;
+        min = Math.floor(min);
+        max = Math.floor(max) - min;
+        return function() {
+          return Math.floor(source() * max + min);
+        };
+      }
+    
+      randomInt.source = sourceRandomInt;
+    
+      return randomInt;
+    })(defaultSource);
+    
+    var normal = (function sourceRandomNormal(source) {
+      function randomNormal(mu, sigma) {
+        var x, r;
+        mu = mu == null ? 0 : +mu;
+        sigma = sigma == null ? 1 : +sigma;
+        return function() {
+          var y;
+    
+          // If available, use the second previously-generated uniform random.
+          if (x != null) y = x, x = null;
+    
+          // Otherwise, generate a new x and y.
+          else do {
+            x = source() * 2 - 1;
+            y = source() * 2 - 1;
+            r = x * x + y * y;
+          } while (!r || r > 1);
+    
+          return mu + sigma * y * Math.sqrt(-2 * Math.log(r) / r);
+        };
+      }
+    
+      randomNormal.source = sourceRandomNormal;
+    
+      return randomNormal;
+    })(defaultSource);
+    
+    var logNormal = (function sourceRandomLogNormal(source) {
+      var N = normal.source(source);
+    
+      function randomLogNormal() {
+        var randomNormal = N.apply(this, arguments);
+        return function() {
+          return Math.exp(randomNormal());
+        };
+      }
+    
+      randomLogNormal.source = sourceRandomLogNormal;
+    
+      return randomLogNormal;
+    })(defaultSource);
+    
+    var irwinHall = (function sourceRandomIrwinHall(source) {
+      function randomIrwinHall(n) {
+        if ((n = +n) <= 0) return () => 0;
+        return function() {
+          for (var sum = 0, i = n; i > 1; --i) sum += source();
+          return sum + i * source();
+        };
+      }
+    
+      randomIrwinHall.source = sourceRandomIrwinHall;
+    
+      return randomIrwinHall;
+    })(defaultSource);
+    
+    var bates = (function sourceRandomBates(source) {
+      var I = irwinHall.source(source);
+    
+      function randomBates(n) {
+        // use limiting distribution at n === 0
+        if ((n = +n) === 0) return source;
+        var randomIrwinHall = I(n);
+        return function() {
+          return randomIrwinHall() / n;
+        };
+      }
+    
+      randomBates.source = sourceRandomBates;
+    
+      return randomBates;
+    })(defaultSource);
+    
+    var exponential = (function sourceRandomExponential(source) {
+      function randomExponential(lambda) {
+        return function() {
+          return -Math.log1p(-source()) / lambda;
+        };
+      }
+    
+      randomExponential.source = sourceRandomExponential;
+    
+      return randomExponential;
+    })(defaultSource);
+    
+    var pareto = (function sourceRandomPareto(source) {
+      function randomPareto(alpha) {
+        if ((alpha = +alpha) < 0) throw new RangeError("invalid alpha");
+        alpha = 1 / -alpha;
+        return function() {
+          return Math.pow(1 - source(), alpha);
+        };
+      }
+    
+      randomPareto.source = sourceRandomPareto;
+    
+      return randomPareto;
+    })(defaultSource);
+    
+    var bernoulli = (function sourceRandomBernoulli(source) {
+      function randomBernoulli(p) {
+        if ((p = +p) < 0 || p > 1) throw new RangeError("invalid p");
+        return function() {
+          return Math.floor(source() + p);
+        };
+      }
+    
+      randomBernoulli.source = sourceRandomBernoulli;
+    
+      return randomBernoulli;
+    })(defaultSource);
+    
+    var geometric = (function sourceRandomGeometric(source) {
+      function randomGeometric(p) {
+        if ((p = +p) < 0 || p > 1) throw new RangeError("invalid p");
+        if (p === 0) return () => Infinity;
+        if (p === 1) return () => 1;
+        p = Math.log1p(-p);
+        return function() {
+          return 1 + Math.floor(Math.log1p(-source()) / p);
+        };
+      }
+    
+      randomGeometric.source = sourceRandomGeometric;
+    
+      return randomGeometric;
+    })(defaultSource);
+    
+    var gamma = (function sourceRandomGamma(source) {
+      var randomNormal = normal.source(source)();
+    
+      function randomGamma(k, theta) {
+        if ((k = +k) < 0) throw new RangeError("invalid k");
+        // degenerate distribution if k === 0
+        if (k === 0) return () => 0;
+        theta = theta == null ? 1 : +theta;
+        // exponential distribution if k === 1
+        if (k === 1) return () => -Math.log1p(-source()) * theta;
+    
+        var d = (k < 1 ? k + 1 : k) - 1 / 3,
+            c = 1 / (3 * Math.sqrt(d)),
+            multiplier = k < 1 ? () => Math.pow(source(), 1 / k) : () => 1;
+        return function() {
+          do {
+            do {
+              var x = randomNormal(),
+                  v = 1 + c * x;
+            } while (v <= 0);
+            v *= v * v;
+            var u = 1 - source();
+          } while (u >= 1 - 0.0331 * x * x * x * x && Math.log(u) >= 0.5 * x * x + d * (1 - v + Math.log(v)));
+          return d * v * multiplier() * theta;
+        };
+      }
+    
+      randomGamma.source = sourceRandomGamma;
+    
+      return randomGamma;
+    })(defaultSource);
+    
+    var beta = (function sourceRandomBeta(source) {
+      var G = gamma.source(source);
+    
+      function randomBeta(alpha, beta) {
+        var X = G(alpha),
+            Y = G(beta);
+        return function() {
+          var x = X();
+          return x === 0 ? 0 : x / (x + Y());
+        };
+      }
+    
+      randomBeta.source = sourceRandomBeta;
+    
+      return randomBeta;
+    })(defaultSource);
+    
+    var binomial = (function sourceRandomBinomial(source) {
+      var G = geometric.source(source),
+          B = beta.source(source);
+    
+      function randomBinomial(n, p) {
+        n = +n;
+        if ((p = +p) >= 1) return () => n;
+        if (p <= 0) return () => 0;
+        return function() {
+          var acc = 0, nn = n, pp = p;
+          while (nn * pp > 16 && nn * (1 - pp) > 16) {
+            var i = Math.floor((nn + 1) * pp),
+                y = B(i, nn - i + 1)();
+            if (y <= pp) {
+              acc += i;
+              nn -= i;
+              pp = (pp - y) / (1 - y);
+            } else {
+              nn = i - 1;
+              pp /= y;
+            }
+          }
+          var sign = pp < 0.5,
+              pFinal = sign ? pp : 1 - pp,
+              g = G(pFinal);
+          for (var s = g(), k = 0; s <= nn; ++k) s += g();
+          return acc + (sign ? k : nn - k);
+        };
+      }
+    
+      randomBinomial.source = sourceRandomBinomial;
+    
+      return randomBinomial;
+    })(defaultSource);
+    
+    var weibull = (function sourceRandomWeibull(source) {
+      function randomWeibull(k, a, b) {
+        var outerFunc;
+        if ((k = +k) === 0) {
+          outerFunc = x => -Math.log(x);
+        } else {
+          k = 1 / k;
+          outerFunc = x => Math.pow(x, k);
+        }
+        a = a == null ? 0 : +a;
+        b = b == null ? 1 : +b;
+        return function() {
+          return a + b * outerFunc(-Math.log1p(-source()));
+        };
+      }
+    
+      randomWeibull.source = sourceRandomWeibull;
+    
+      return randomWeibull;
+    })(defaultSource);
+    
+    var cauchy = (function sourceRandomCauchy(source) {
+      function randomCauchy(a, b) {
+        a = a == null ? 0 : +a;
+        b = b == null ? 1 : +b;
+        return function() {
+          return a + b * Math.tan(Math.PI * source());
+        };
+      }
+    
+      randomCauchy.source = sourceRandomCauchy;
+    
+      return randomCauchy;
+    })(defaultSource);
+    
+    var logistic = (function sourceRandomLogistic(source) {
+      function randomLogistic(a, b) {
+        a = a == null ? 0 : +a;
+        b = b == null ? 1 : +b;
+        return function() {
+          var u = source();
+          return a + b * Math.log(u / (1 - u));
+        };
+      }
+    
+      randomLogistic.source = sourceRandomLogistic;
+    
+      return randomLogistic;
+    })(defaultSource);
+    
+    var poisson = (function sourceRandomPoisson(source) {
+      var G = gamma.source(source),
+          B = binomial.source(source);
+    
+      function randomPoisson(lambda) {
+        return function() {
+          var acc = 0, l = lambda;
+          while (l > 16) {
+            var n = Math.floor(0.875 * l),
+                t = G(n)();
+            if (t > l) return acc + B(n - 1, l / t)();
+            acc += n;
+            l -= t;
+          }
+          for (var s = -Math.log1p(-source()), k = 0; s <= l; ++k) s -= Math.log1p(-source());
+          return acc + k;
+        };
+      }
+    
+      randomPoisson.source = sourceRandomPoisson;
+    
+      return randomPoisson;
+    })(defaultSource);
+    
+    // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
+    const mul = 0x19660D;
+    const inc = 0x3C6EF35F;
+    const eps = 1 / 0x100000000;
+    
+    function lcg(seed = Math.random()) {
+      let state = (0 <= seed && seed < 1 ? seed / eps : Math.abs(seed)) | 0;
+      return () => (state = mul * state + inc | 0, eps * (state >>> 0));
+    }
+    
+    function initRange(domain, range) {
+      switch (arguments.length) {
+        case 0: break;
+        case 1: this.range(domain); break;
+        default: this.range(range).domain(domain); break;
+      }
+      return this;
+    }
+    
+    function initInterpolator(domain, interpolator) {
+      switch (arguments.length) {
+        case 0: break;
+        case 1: {
+          if (typeof domain === "function") this.interpolator(domain);
+          else this.range(domain);
+          break;
+        }
+        default: {
+          this.domain(domain);
+          if (typeof interpolator === "function") this.interpolator(interpolator);
+          else this.range(interpolator);
+          break;
+        }
+      }
+      return this;
+    }
+    
+    const implicit = Symbol("implicit");
+    
+    function ordinal() {
+      var index = new Map(),
+          domain = [],
+          range = [],
+          unknown = implicit;
+    
+      function scale(d) {
+        var key = d + "", i = index.get(key);
+        if (!i) {
+          if (unknown !== implicit) return unknown;
+          index.set(key, i = domain.push(d));
+        }
+        return range[(i - 1) % range.length];
+      }
+    
+      scale.domain = function(_) {
+        if (!arguments.length) return domain.slice();
+        domain = [], index = new Map();
+        for (const value of _) {
+          const key = value + "";
+          if (index.has(key)) continue;
+          index.set(key, domain.push(value));
+        }
+        return scale;
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (range = Array.from(_), scale) : range.slice();
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      scale.copy = function() {
+        return ordinal(domain, range).unknown(unknown);
+      };
+    
+      initRange.apply(scale, arguments);
+    
+      return scale;
+    }
+    
+    function band() {
+      var scale = ordinal().unknown(undefined),
+          domain = scale.domain,
+          ordinalRange = scale.range,
+          r0 = 0,
+          r1 = 1,
+          step,
+          bandwidth,
+          round = false,
+          paddingInner = 0,
+          paddingOuter = 0,
+          align = 0.5;
+    
+      delete scale.unknown;
+    
+      function rescale() {
+        var n = domain().length,
+            reverse = r1 < r0,
+            start = reverse ? r1 : r0,
+            stop = reverse ? r0 : r1;
+        step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2);
+        if (round) step = Math.floor(step);
+        start += (stop - start - step * (n - paddingInner)) * align;
+        bandwidth = step * (1 - paddingInner);
+        if (round) start = Math.round(start), bandwidth = Math.round(bandwidth);
+        var values = sequence(n).map(function(i) { return start + step * i; });
+        return ordinalRange(reverse ? values.reverse() : values);
+      }
+    
+      scale.domain = function(_) {
+        return arguments.length ? (domain(_), rescale()) : domain();
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? ([r0, r1] = _, r0 = +r0, r1 = +r1, rescale()) : [r0, r1];
+      };
+    
+      scale.rangeRound = function(_) {
+        return [r0, r1] = _, r0 = +r0, r1 = +r1, round = true, rescale();
+      };
+    
+      scale.bandwidth = function() {
+        return bandwidth;
+      };
+    
+      scale.step = function() {
+        return step;
+      };
+    
+      scale.round = function(_) {
+        return arguments.length ? (round = !!_, rescale()) : round;
+      };
+    
+      scale.padding = function(_) {
+        return arguments.length ? (paddingInner = Math.min(1, paddingOuter = +_), rescale()) : paddingInner;
+      };
+    
+      scale.paddingInner = function(_) {
+        return arguments.length ? (paddingInner = Math.min(1, _), rescale()) : paddingInner;
+      };
+    
+      scale.paddingOuter = function(_) {
+        return arguments.length ? (paddingOuter = +_, rescale()) : paddingOuter;
+      };
+    
+      scale.align = function(_) {
+        return arguments.length ? (align = Math.max(0, Math.min(1, _)), rescale()) : align;
+      };
+    
+      scale.copy = function() {
+        return band(domain(), [r0, r1])
+            .round(round)
+            .paddingInner(paddingInner)
+            .paddingOuter(paddingOuter)
+            .align(align);
+      };
+    
+      return initRange.apply(rescale(), arguments);
+    }
+    
+    function pointish(scale) {
+      var copy = scale.copy;
+    
+      scale.padding = scale.paddingOuter;
+      delete scale.paddingInner;
+      delete scale.paddingOuter;
+    
+      scale.copy = function() {
+        return pointish(copy());
+      };
+    
+      return scale;
+    }
+    
+    function point$4() {
+      return pointish(band.apply(null, arguments).paddingInner(1));
+    }
+    
+    function constants(x) {
+      return function() {
+        return x;
+      };
+    }
+    
+    function number$1(x) {
+      return +x;
+    }
+    
+    var unit = [0, 1];
+    
+    function identity$3(x) {
+      return x;
+    }
+    
+    function normalize(a, b) {
+      return (b -= (a = +a))
+          ? function(x) { return (x - a) / b; }
+          : constants(isNaN(b) ? NaN : 0.5);
+    }
+    
+    function clamper(a, b) {
+      var t;
+      if (a > b) t = a, a = b, b = t;
+      return function(x) { return Math.max(a, Math.min(b, x)); };
+    }
+    
+    // normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].
+    // interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b].
+    function bimap(domain, range, interpolate) {
+      var d0 = domain[0], d1 = domain[1], r0 = range[0], r1 = range[1];
+      if (d1 < d0) d0 = normalize(d1, d0), r0 = interpolate(r1, r0);
+      else d0 = normalize(d0, d1), r0 = interpolate(r0, r1);
+      return function(x) { return r0(d0(x)); };
+    }
+    
+    function polymap(domain, range, interpolate) {
+      var j = Math.min(domain.length, range.length) - 1,
+          d = new Array(j),
+          r = new Array(j),
+          i = -1;
+    
+      // Reverse descending domains.
+      if (domain[j] < domain[0]) {
+        domain = domain.slice().reverse();
+        range = range.slice().reverse();
+      }
+    
+      while (++i < j) {
+        d[i] = normalize(domain[i], domain[i + 1]);
+        r[i] = interpolate(range[i], range[i + 1]);
+      }
+    
+      return function(x) {
+        var i = bisectRight(domain, x, 1, j) - 1;
+        return r[i](d[i](x));
+      };
+    }
+    
+    function copy$1(source, target) {
+      return target
+          .domain(source.domain())
+          .range(source.range())
+          .interpolate(source.interpolate())
+          .clamp(source.clamp())
+          .unknown(source.unknown());
+    }
+    
+    function transformer$2() {
+      var domain = unit,
+          range = unit,
+          interpolate = interpolate$2,
+          transform,
+          untransform,
+          unknown,
+          clamp = identity$3,
+          piecewise,
+          output,
+          input;
+    
+      function rescale() {
+        var n = Math.min(domain.length, range.length);
+        if (clamp !== identity$3) clamp = clamper(domain[0], domain[n - 1]);
+        piecewise = n > 2 ? polymap : bimap;
+        output = input = null;
+        return scale;
+      }
+    
+      function scale(x) {
+        return x == null || isNaN(x = +x) ? unknown : (output || (output = piecewise(domain.map(transform), range, interpolate)))(transform(clamp(x)));
+      }
+    
+      scale.invert = function(y) {
+        return clamp(untransform((input || (input = piecewise(range, domain.map(transform), interpolateNumber)))(y)));
+      };
+    
+      scale.domain = function(_) {
+        return arguments.length ? (domain = Array.from(_, number$1), rescale()) : domain.slice();
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (range = Array.from(_), rescale()) : range.slice();
+      };
+    
+      scale.rangeRound = function(_) {
+        return range = Array.from(_), interpolate = interpolateRound, rescale();
+      };
+    
+      scale.clamp = function(_) {
+        return arguments.length ? (clamp = _ ? true : identity$3, rescale()) : clamp !== identity$3;
+      };
+    
+      scale.interpolate = function(_) {
+        return arguments.length ? (interpolate = _, rescale()) : interpolate;
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      return function(t, u) {
+        transform = t, untransform = u;
+        return rescale();
+      };
+    }
+    
+    function continuous() {
+      return transformer$2()(identity$3, identity$3);
+    }
+    
+    function tickFormat(start, stop, count, specifier) {
+      var step = tickStep(start, stop, count),
+          precision;
+      specifier = formatSpecifier(specifier == null ? ",f" : specifier);
+      switch (specifier.type) {
+        case "s": {
+          var value = Math.max(Math.abs(start), Math.abs(stop));
+          if (specifier.precision == null && !isNaN(precision = precisionPrefix(step, value))) specifier.precision = precision;
+          return exports.formatPrefix(specifier, value);
+        }
+        case "":
+        case "e":
+        case "g":
+        case "p":
+        case "r": {
+          if (specifier.precision == null && !isNaN(precision = precisionRound(step, Math.max(Math.abs(start), Math.abs(stop))))) specifier.precision = precision - (specifier.type === "e");
+          break;
+        }
+        case "f":
+        case "%": {
+          if (specifier.precision == null && !isNaN(precision = precisionFixed(step))) specifier.precision = precision - (specifier.type === "%") * 2;
+          break;
+        }
+      }
+      return exports.format(specifier);
+    }
+    
+    function linearish(scale) {
+      var domain = scale.domain;
+    
+      scale.ticks = function(count) {
+        var d = domain();
+        return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
+      };
+    
+      scale.tickFormat = function(count, specifier) {
+        var d = domain();
+        return tickFormat(d[0], d[d.length - 1], count == null ? 10 : count, specifier);
+      };
+    
+      scale.nice = function(count) {
+        if (count == null) count = 10;
+    
+        var d = domain();
+        var i0 = 0;
+        var i1 = d.length - 1;
+        var start = d[i0];
+        var stop = d[i1];
+        var prestep;
+        var step;
+        var maxIter = 10;
+    
+        if (stop < start) {
+          step = start, start = stop, stop = step;
+          step = i0, i0 = i1, i1 = step;
+        }
+        
+        while (maxIter-- > 0) {
+          step = tickIncrement(start, stop, count);
+          if (step === prestep) {
+            d[i0] = start;
+            d[i1] = stop;
+            return domain(d);
+          } else if (step > 0) {
+            start = Math.floor(start / step) * step;
+            stop = Math.ceil(stop / step) * step;
+          } else if (step < 0) {
+            start = Math.ceil(start * step) / step;
+            stop = Math.floor(stop * step) / step;
+          } else {
+            break;
+          }
+          prestep = step;
+        }
+    
+        return scale;
+      };
+    
+      return scale;
+    }
+    
+    function linear() {
+      var scale = continuous();
+    
+      scale.copy = function() {
+        return copy$1(scale, linear());
+      };
+    
+      initRange.apply(scale, arguments);
+    
+      return linearish(scale);
+    }
+    
+    function identity$2(domain) {
+      var unknown;
+    
+      function scale(x) {
+        return x == null || isNaN(x = +x) ? unknown : x;
+      }
+    
+      scale.invert = scale;
+    
+      scale.domain = scale.range = function(_) {
+        return arguments.length ? (domain = Array.from(_, number$1), scale) : domain.slice();
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      scale.copy = function() {
+        return identity$2(domain).unknown(unknown);
+      };
+    
+      domain = arguments.length ? Array.from(domain, number$1) : [0, 1];
+    
+      return linearish(scale);
+    }
+    
+    function nice(domain, interval) {
+      domain = domain.slice();
+    
+      var i0 = 0,
+          i1 = domain.length - 1,
+          x0 = domain[i0],
+          x1 = domain[i1],
+          t;
+    
+      if (x1 < x0) {
+        t = i0, i0 = i1, i1 = t;
+        t = x0, x0 = x1, x1 = t;
+      }
+    
+      domain[i0] = interval.floor(x0);
+      domain[i1] = interval.ceil(x1);
+      return domain;
+    }
+    
+    function transformLog(x) {
+      return Math.log(x);
+    }
+    
+    function transformExp(x) {
+      return Math.exp(x);
+    }
+    
+    function transformLogn(x) {
+      return -Math.log(-x);
+    }
+    
+    function transformExpn(x) {
+      return -Math.exp(-x);
+    }
+    
+    function pow10(x) {
+      return isFinite(x) ? +("1e" + x) : x < 0 ? 0 : x;
+    }
+    
+    function powp(base) {
+      return base === 10 ? pow10
+          : base === Math.E ? Math.exp
+          : function(x) { return Math.pow(base, x); };
+    }
+    
+    function logp(base) {
+      return base === Math.E ? Math.log
+          : base === 10 && Math.log10
+          || base === 2 && Math.log2
+          || (base = Math.log(base), function(x) { return Math.log(x) / base; });
+    }
+    
+    function reflect(f) {
+      return function(x) {
+        return -f(-x);
+      };
+    }
+    
+    function loggish(transform) {
+      var scale = transform(transformLog, transformExp),
+          domain = scale.domain,
+          base = 10,
+          logs,
+          pows;
+    
+      function rescale() {
+        logs = logp(base), pows = powp(base);
+        if (domain()[0] < 0) {
+          logs = reflect(logs), pows = reflect(pows);
+          transform(transformLogn, transformExpn);
+        } else {
+          transform(transformLog, transformExp);
+        }
+        return scale;
+      }
+    
+      scale.base = function(_) {
+        return arguments.length ? (base = +_, rescale()) : base;
+      };
+    
+      scale.domain = function(_) {
+        return arguments.length ? (domain(_), rescale()) : domain();
+      };
+    
+      scale.ticks = function(count) {
+        var d = domain(),
+            u = d[0],
+            v = d[d.length - 1],
+            r;
+    
+        if (r = v < u) i = u, u = v, v = i;
+    
+        var i = logs(u),
+            j = logs(v),
+            p,
+            k,
+            t,
+            n = count == null ? 10 : +count,
+            z = [];
+    
+        if (!(base % 1) && j - i < n) {
+          i = Math.floor(i), j = Math.ceil(j);
+          if (u > 0) for (; i <= j; ++i) {
+            for (k = 1, p = pows(i); k < base; ++k) {
+              t = p * k;
+              if (t < u) continue;
+              if (t > v) break;
+              z.push(t);
+            }
+          } else for (; i <= j; ++i) {
+            for (k = base - 1, p = pows(i); k >= 1; --k) {
+              t = p * k;
+              if (t < u) continue;
+              if (t > v) break;
+              z.push(t);
+            }
+          }
+          if (z.length * 2 < n) z = ticks(u, v, n);
+        } else {
+          z = ticks(i, j, Math.min(j - i, n)).map(pows);
+        }
+    
+        return r ? z.reverse() : z;
+      };
+    
+      scale.tickFormat = function(count, specifier) {
+        if (specifier == null) specifier = base === 10 ? ".0e" : ",";
+        if (typeof specifier !== "function") specifier = exports.format(specifier);
+        if (count === Infinity) return specifier;
+        if (count == null) count = 10;
+        var k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?
+        return function(d) {
+          var i = d / pows(Math.round(logs(d)));
+          if (i * base < base - 0.5) i *= base;
+          return i <= k ? specifier(d) : "";
+        };
+      };
+    
+      scale.nice = function() {
+        return domain(nice(domain(), {
+          floor: function(x) { return pows(Math.floor(logs(x))); },
+          ceil: function(x) { return pows(Math.ceil(logs(x))); }
+        }));
+      };
+    
+      return scale;
+    }
+    
+    function log() {
+      var scale = loggish(transformer$2()).domain([1, 10]);
+    
+      scale.copy = function() {
+        return copy$1(scale, log()).base(scale.base());
+      };
+    
+      initRange.apply(scale, arguments);
+    
+      return scale;
+    }
+    
+    function transformSymlog(c) {
+      return function(x) {
+        return Math.sign(x) * Math.log1p(Math.abs(x / c));
+      };
+    }
+    
+    function transformSymexp(c) {
+      return function(x) {
+        return Math.sign(x) * Math.expm1(Math.abs(x)) * c;
+      };
+    }
+    
+    function symlogish(transform) {
+      var c = 1, scale = transform(transformSymlog(c), transformSymexp(c));
+    
+      scale.constant = function(_) {
+        return arguments.length ? transform(transformSymlog(c = +_), transformSymexp(c)) : c;
+      };
+    
+      return linearish(scale);
+    }
+    
+    function symlog() {
+      var scale = symlogish(transformer$2());
+    
+      scale.copy = function() {
+        return copy$1(scale, symlog()).constant(scale.constant());
+      };
+    
+      return initRange.apply(scale, arguments);
+    }
+    
+    function transformPow(exponent) {
+      return function(x) {
+        return x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent);
+      };
+    }
+    
+    function transformSqrt(x) {
+      return x < 0 ? -Math.sqrt(-x) : Math.sqrt(x);
+    }
+    
+    function transformSquare(x) {
+      return x < 0 ? -x * x : x * x;
+    }
+    
+    function powish(transform) {
+      var scale = transform(identity$3, identity$3),
+          exponent = 1;
+    
+      function rescale() {
+        return exponent === 1 ? transform(identity$3, identity$3)
+            : exponent === 0.5 ? transform(transformSqrt, transformSquare)
+            : transform(transformPow(exponent), transformPow(1 / exponent));
+      }
+    
+      scale.exponent = function(_) {
+        return arguments.length ? (exponent = +_, rescale()) : exponent;
+      };
+    
+      return linearish(scale);
+    }
+    
+    function pow() {
+      var scale = powish(transformer$2());
+    
+      scale.copy = function() {
+        return copy$1(scale, pow()).exponent(scale.exponent());
+      };
+    
+      initRange.apply(scale, arguments);
+    
+      return scale;
+    }
+    
+    function sqrt$1() {
+      return pow.apply(null, arguments).exponent(0.5);
+    }
+    
+    function square$1(x) {
+      return Math.sign(x) * x * x;
+    }
+    
+    function unsquare(x) {
+      return Math.sign(x) * Math.sqrt(Math.abs(x));
+    }
+    
+    function radial() {
+      var squared = continuous(),
+          range = [0, 1],
+          round = false,
+          unknown;
+    
+      function scale(x) {
+        var y = unsquare(squared(x));
+        return isNaN(y) ? unknown : round ? Math.round(y) : y;
+      }
+    
+      scale.invert = function(y) {
+        return squared.invert(square$1(y));
+      };
+    
+      scale.domain = function(_) {
+        return arguments.length ? (squared.domain(_), scale) : squared.domain();
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (squared.range((range = Array.from(_, number$1)).map(square$1)), scale) : range.slice();
+      };
+    
+      scale.rangeRound = function(_) {
+        return scale.range(_).round(true);
+      };
+    
+      scale.round = function(_) {
+        return arguments.length ? (round = !!_, scale) : round;
+      };
+    
+      scale.clamp = function(_) {
+        return arguments.length ? (squared.clamp(_), scale) : squared.clamp();
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      scale.copy = function() {
+        return radial(squared.domain(), range)
+            .round(round)
+            .clamp(squared.clamp())
+            .unknown(unknown);
+      };
+    
+      initRange.apply(scale, arguments);
+    
+      return linearish(scale);
+    }
+    
+    function quantile() {
+      var domain = [],
+          range = [],
+          thresholds = [],
+          unknown;
+    
+      function rescale() {
+        var i = 0, n = Math.max(1, range.length);
+        thresholds = new Array(n - 1);
+        while (++i < n) thresholds[i - 1] = quantileSorted(domain, i / n);
+        return scale;
+      }
+    
+      function scale(x) {
+        return x == null || isNaN(x = +x) ? unknown : range[bisectRight(thresholds, x)];
+      }
+    
+      scale.invertExtent = function(y) {
+        var i = range.indexOf(y);
+        return i < 0 ? [NaN, NaN] : [
+          i > 0 ? thresholds[i - 1] : domain[0],
+          i < thresholds.length ? thresholds[i] : domain[domain.length - 1]
+        ];
+      };
+    
+      scale.domain = function(_) {
+        if (!arguments.length) return domain.slice();
+        domain = [];
+        for (let d of _) if (d != null && !isNaN(d = +d)) domain.push(d);
+        domain.sort(ascending$3);
+        return rescale();
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (range = Array.from(_), rescale()) : range.slice();
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      scale.quantiles = function() {
+        return thresholds.slice();
+      };
+    
+      scale.copy = function() {
+        return quantile()
+            .domain(domain)
+            .range(range)
+            .unknown(unknown);
+      };
+    
+      return initRange.apply(scale, arguments);
+    }
+    
+    function quantize() {
+      var x0 = 0,
+          x1 = 1,
+          n = 1,
+          domain = [0.5],
+          range = [0, 1],
+          unknown;
+    
+      function scale(x) {
+        return x != null && x <= x ? range[bisectRight(domain, x, 0, n)] : unknown;
+      }
+    
+      function rescale() {
+        var i = -1;
+        domain = new Array(n);
+        while (++i < n) domain[i] = ((i + 1) * x1 - (i - n) * x0) / (n + 1);
+        return scale;
+      }
+    
+      scale.domain = function(_) {
+        return arguments.length ? ([x0, x1] = _, x0 = +x0, x1 = +x1, rescale()) : [x0, x1];
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (n = (range = Array.from(_)).length - 1, rescale()) : range.slice();
+      };
+    
+      scale.invertExtent = function(y) {
+        var i = range.indexOf(y);
+        return i < 0 ? [NaN, NaN]
+            : i < 1 ? [x0, domain[0]]
+            : i >= n ? [domain[n - 1], x1]
+            : [domain[i - 1], domain[i]];
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : scale;
+      };
+    
+      scale.thresholds = function() {
+        return domain.slice();
+      };
+    
+      scale.copy = function() {
+        return quantize()
+            .domain([x0, x1])
+            .range(range)
+            .unknown(unknown);
+      };
+    
+      return initRange.apply(linearish(scale), arguments);
+    }
+    
+    function threshold() {
+      var domain = [0.5],
+          range = [0, 1],
+          unknown,
+          n = 1;
+    
+      function scale(x) {
+        return x != null && x <= x ? range[bisectRight(domain, x, 0, n)] : unknown;
+      }
+    
+      scale.domain = function(_) {
+        return arguments.length ? (domain = Array.from(_), n = Math.min(domain.length, range.length - 1), scale) : domain.slice();
+      };
+    
+      scale.range = function(_) {
+        return arguments.length ? (range = Array.from(_), n = Math.min(domain.length, range.length - 1), scale) : range.slice();
+      };
+    
+      scale.invertExtent = function(y) {
+        var i = range.indexOf(y);
+        return [domain[i - 1], domain[i]];
+      };
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      scale.copy = function() {
+        return threshold()
+            .domain(domain)
+            .range(range)
+            .unknown(unknown);
+      };
+    
+      return initRange.apply(scale, arguments);
+    }
+    
+    var t0 = new Date,
+        t1 = new Date;
+    
+    function newInterval(floori, offseti, count, field) {
+    
+      function interval(date) {
+        return floori(date = arguments.length === 0 ? new Date : new Date(+date)), date;
+      }
+    
+      interval.floor = function(date) {
+        return floori(date = new Date(+date)), date;
+      };
+    
+      interval.ceil = function(date) {
+        return floori(date = new Date(date - 1)), offseti(date, 1), floori(date), date;
+      };
+    
+      interval.round = function(date) {
+        var d0 = interval(date),
+            d1 = interval.ceil(date);
+        return date - d0 < d1 - date ? d0 : d1;
+      };
+    
+      interval.offset = function(date, step) {
+        return offseti(date = new Date(+date), step == null ? 1 : Math.floor(step)), date;
+      };
+    
+      interval.range = function(start, stop, step) {
+        var range = [], previous;
+        start = interval.ceil(start);
+        step = step == null ? 1 : Math.floor(step);
+        if (!(start < stop) || !(step > 0)) return range; // also handles Invalid Date
+        do range.push(previous = new Date(+start)), offseti(start, step), floori(start);
+        while (previous < start && start < stop);
+        return range;
+      };
+    
+      interval.filter = function(test) {
+        return newInterval(function(date) {
+          if (date >= date) while (floori(date), !test(date)) date.setTime(date - 1);
+        }, function(date, step) {
+          if (date >= date) {
+            if (step < 0) while (++step <= 0) {
+              while (offseti(date, -1), !test(date)) {} // eslint-disable-line no-empty
+            } else while (--step >= 0) {
+              while (offseti(date, +1), !test(date)) {} // eslint-disable-line no-empty
+            }
+          }
+        });
+      };
+    
+      if (count) {
+        interval.count = function(start, end) {
+          t0.setTime(+start), t1.setTime(+end);
+          floori(t0), floori(t1);
+          return Math.floor(count(t0, t1));
+        };
+    
+        interval.every = function(step) {
+          step = Math.floor(step);
+          return !isFinite(step) || !(step > 0) ? null
+              : !(step > 1) ? interval
+              : interval.filter(field
+                  ? function(d) { return field(d) % step === 0; }
+                  : function(d) { return interval.count(0, d) % step === 0; });
+        };
+      }
+    
+      return interval;
+    }
+    
+    var millisecond = newInterval(function() {
+      // noop
+    }, function(date, step) {
+      date.setTime(+date + step);
+    }, function(start, end) {
+      return end - start;
+    });
+    
+    // An optimized implementation for this simple case.
+    millisecond.every = function(k) {
+      k = Math.floor(k);
+      if (!isFinite(k) || !(k > 0)) return null;
+      if (!(k > 1)) return millisecond;
+      return newInterval(function(date) {
+        date.setTime(Math.floor(date / k) * k);
+      }, function(date, step) {
+        date.setTime(+date + step * k);
+      }, function(start, end) {
+        return (end - start) / k;
+      });
+    };
+    var milliseconds = millisecond.range;
+    
+    const durationSecond = 1000;
+    const durationMinute = durationSecond * 60;
+    const durationHour = durationMinute * 60;
+    const durationDay = durationHour * 24;
+    const durationWeek = durationDay * 7;
+    const durationMonth = durationDay * 30;
+    const durationYear = durationDay * 365;
+    
+    var second = newInterval(function(date) {
+      date.setTime(date - date.getMilliseconds());
+    }, function(date, step) {
+      date.setTime(+date + step * durationSecond);
+    }, function(start, end) {
+      return (end - start) / durationSecond;
+    }, function(date) {
+      return date.getUTCSeconds();
+    });
+    var seconds = second.range;
+    
+    var minute = newInterval(function(date) {
+      date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond);
+    }, function(date, step) {
+      date.setTime(+date + step * durationMinute);
+    }, function(start, end) {
+      return (end - start) / durationMinute;
+    }, function(date) {
+      return date.getMinutes();
+    });
+    var minutes = minute.range;
+    
+    var hour = newInterval(function(date) {
+      date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute);
+    }, function(date, step) {
+      date.setTime(+date + step * durationHour);
+    }, function(start, end) {
+      return (end - start) / durationHour;
+    }, function(date) {
+      return date.getHours();
+    });
+    var hours = hour.range;
+    
+    var day = newInterval(
+      date => date.setHours(0, 0, 0, 0),
+      (date, step) => date.setDate(date.getDate() + step),
+      (start, end) => (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationDay,
+      date => date.getDate() - 1
+    );
+    var days = day.range;
+    
+    function weekday(i) {
+      return newInterval(function(date) {
+        date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);
+        date.setHours(0, 0, 0, 0);
+      }, function(date, step) {
+        date.setDate(date.getDate() + step * 7);
+      }, function(start, end) {
+        return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek;
+      });
+    }
+    
+    var sunday = weekday(0);
+    var monday = weekday(1);
+    var tuesday = weekday(2);
+    var wednesday = weekday(3);
+    var thursday = weekday(4);
+    var friday = weekday(5);
+    var saturday = weekday(6);
+    
+    var sundays = sunday.range;
+    var mondays = monday.range;
+    var tuesdays = tuesday.range;
+    var wednesdays = wednesday.range;
+    var thursdays = thursday.range;
+    var fridays = friday.range;
+    var saturdays = saturday.range;
+    
+    var month = newInterval(function(date) {
+      date.setDate(1);
+      date.setHours(0, 0, 0, 0);
+    }, function(date, step) {
+      date.setMonth(date.getMonth() + step);
+    }, function(start, end) {
+      return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;
+    }, function(date) {
+      return date.getMonth();
+    });
+    var months = month.range;
+    
+    var year = newInterval(function(date) {
+      date.setMonth(0, 1);
+      date.setHours(0, 0, 0, 0);
+    }, function(date, step) {
+      date.setFullYear(date.getFullYear() + step);
+    }, function(start, end) {
+      return end.getFullYear() - start.getFullYear();
+    }, function(date) {
+      return date.getFullYear();
+    });
+    
+    // An optimized implementation for this simple case.
+    year.every = function(k) {
+      return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : newInterval(function(date) {
+        date.setFullYear(Math.floor(date.getFullYear() / k) * k);
+        date.setMonth(0, 1);
+        date.setHours(0, 0, 0, 0);
+      }, function(date, step) {
+        date.setFullYear(date.getFullYear() + step * k);
+      });
+    };
+    var years = year.range;
+    
+    var utcMinute = newInterval(function(date) {
+      date.setUTCSeconds(0, 0);
+    }, function(date, step) {
+      date.setTime(+date + step * durationMinute);
+    }, function(start, end) {
+      return (end - start) / durationMinute;
+    }, function(date) {
+      return date.getUTCMinutes();
+    });
+    var utcMinutes = utcMinute.range;
+    
+    var utcHour = newInterval(function(date) {
+      date.setUTCMinutes(0, 0, 0);
+    }, function(date, step) {
+      date.setTime(+date + step * durationHour);
+    }, function(start, end) {
+      return (end - start) / durationHour;
+    }, function(date) {
+      return date.getUTCHours();
+    });
+    var utcHours = utcHour.range;
+    
+    var utcDay = newInterval(function(date) {
+      date.setUTCHours(0, 0, 0, 0);
+    }, function(date, step) {
+      date.setUTCDate(date.getUTCDate() + step);
+    }, function(start, end) {
+      return (end - start) / durationDay;
+    }, function(date) {
+      return date.getUTCDate() - 1;
+    });
+    var utcDays = utcDay.range;
+    
+    function utcWeekday(i) {
+      return newInterval(function(date) {
+        date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7);
+        date.setUTCHours(0, 0, 0, 0);
+      }, function(date, step) {
+        date.setUTCDate(date.getUTCDate() + step * 7);
+      }, function(start, end) {
+        return (end - start) / durationWeek;
+      });
+    }
+    
+    var utcSunday = utcWeekday(0);
+    var utcMonday = utcWeekday(1);
+    var utcTuesday = utcWeekday(2);
+    var utcWednesday = utcWeekday(3);
+    var utcThursday = utcWeekday(4);
+    var utcFriday = utcWeekday(5);
+    var utcSaturday = utcWeekday(6);
+    
+    var utcSundays = utcSunday.range;
+    var utcMondays = utcMonday.range;
+    var utcTuesdays = utcTuesday.range;
+    var utcWednesdays = utcWednesday.range;
+    var utcThursdays = utcThursday.range;
+    var utcFridays = utcFriday.range;
+    var utcSaturdays = utcSaturday.range;
+    
+    var utcMonth = newInterval(function(date) {
+      date.setUTCDate(1);
+      date.setUTCHours(0, 0, 0, 0);
+    }, function(date, step) {
+      date.setUTCMonth(date.getUTCMonth() + step);
+    }, function(start, end) {
+      return end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12;
+    }, function(date) {
+      return date.getUTCMonth();
+    });
+    var utcMonths = utcMonth.range;
+    
+    var utcYear = newInterval(function(date) {
+      date.setUTCMonth(0, 1);
+      date.setUTCHours(0, 0, 0, 0);
+    }, function(date, step) {
+      date.setUTCFullYear(date.getUTCFullYear() + step);
+    }, function(start, end) {
+      return end.getUTCFullYear() - start.getUTCFullYear();
+    }, function(date) {
+      return date.getUTCFullYear();
+    });
+    
+    // An optimized implementation for this simple case.
+    utcYear.every = function(k) {
+      return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : newInterval(function(date) {
+        date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k);
+        date.setUTCMonth(0, 1);
+        date.setUTCHours(0, 0, 0, 0);
+      }, function(date, step) {
+        date.setUTCFullYear(date.getUTCFullYear() + step * k);
+      });
+    };
+    var utcYears = utcYear.range;
+    
+    function ticker(year, month, week, day, hour, minute) {
+    
+      const tickIntervals = [
+        [second,  1,      durationSecond],
+        [second,  5,  5 * durationSecond],
+        [second, 15, 15 * durationSecond],
+        [second, 30, 30 * durationSecond],
+        [minute,  1,      durationMinute],
+        [minute,  5,  5 * durationMinute],
+        [minute, 15, 15 * durationMinute],
+        [minute, 30, 30 * durationMinute],
+        [  hour,  1,      durationHour  ],
+        [  hour,  3,  3 * durationHour  ],
+        [  hour,  6,  6 * durationHour  ],
+        [  hour, 12, 12 * durationHour  ],
+        [   day,  1,      durationDay   ],
+        [   day,  2,  2 * durationDay   ],
+        [  week,  1,      durationWeek  ],
+        [ month,  1,      durationMonth ],
+        [ month,  3,  3 * durationMonth ],
+        [  year,  1,      durationYear  ]
+      ];
+    
+      function ticks(start, stop, count) {
+        const reverse = stop < start;
+        if (reverse) [start, stop] = [stop, start];
+        const interval = count && typeof count.range === "function" ? count : tickInterval(start, stop, count);
+        const ticks = interval ? interval.range(start, +stop + 1) : []; // inclusive stop
+        return reverse ? ticks.reverse() : ticks;
+      }
+    
+      function tickInterval(start, stop, count) {
+        const target = Math.abs(stop - start) / count;
+        const i = bisector(([,, step]) => step).right(tickIntervals, target);
+        if (i === tickIntervals.length) return year.every(tickStep(start / durationYear, stop / durationYear, count));
+        if (i === 0) return millisecond.every(Math.max(tickStep(start, stop, count), 1));
+        const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];
+        return t.every(step);
+      }
+    
+      return [ticks, tickInterval];
+    }
+    
+    const [utcTicks, utcTickInterval] = ticker(utcYear, utcMonth, utcSunday, utcDay, utcHour, utcMinute);
+    const [timeTicks, timeTickInterval] = ticker(year, month, sunday, day, hour, minute);
+    
+    function localDate(d) {
+      if (0 <= d.y && d.y < 100) {
+        var date = new Date(-1, d.m, d.d, d.H, d.M, d.S, d.L);
+        date.setFullYear(d.y);
+        return date;
+      }
+      return new Date(d.y, d.m, d.d, d.H, d.M, d.S, d.L);
+    }
+    
+    function utcDate(d) {
+      if (0 <= d.y && d.y < 100) {
+        var date = new Date(Date.UTC(-1, d.m, d.d, d.H, d.M, d.S, d.L));
+        date.setUTCFullYear(d.y);
+        return date;
+      }
+      return new Date(Date.UTC(d.y, d.m, d.d, d.H, d.M, d.S, d.L));
+    }
+    
+    function newDate(y, m, d) {
+      return {y: y, m: m, d: d, H: 0, M: 0, S: 0, L: 0};
+    }
+    
+    function formatLocale(locale) {
+      var locale_dateTime = locale.dateTime,
+          locale_date = locale.date,
+          locale_time = locale.time,
+          locale_periods = locale.periods,
+          locale_weekdays = locale.days,
+          locale_shortWeekdays = locale.shortDays,
+          locale_months = locale.months,
+          locale_shortMonths = locale.shortMonths;
+    
+      var periodRe = formatRe(locale_periods),
+          periodLookup = formatLookup(locale_periods),
+          weekdayRe = formatRe(locale_weekdays),
+          weekdayLookup = formatLookup(locale_weekdays),
+          shortWeekdayRe = formatRe(locale_shortWeekdays),
+          shortWeekdayLookup = formatLookup(locale_shortWeekdays),
+          monthRe = formatRe(locale_months),
+          monthLookup = formatLookup(locale_months),
+          shortMonthRe = formatRe(locale_shortMonths),
+          shortMonthLookup = formatLookup(locale_shortMonths);
+    
+      var formats = {
+        "a": formatShortWeekday,
+        "A": formatWeekday,
+        "b": formatShortMonth,
+        "B": formatMonth,
+        "c": null,
+        "d": formatDayOfMonth,
+        "e": formatDayOfMonth,
+        "f": formatMicroseconds,
+        "g": formatYearISO,
+        "G": formatFullYearISO,
+        "H": formatHour24,
+        "I": formatHour12,
+        "j": formatDayOfYear,
+        "L": formatMilliseconds,
+        "m": formatMonthNumber,
+        "M": formatMinutes,
+        "p": formatPeriod,
+        "q": formatQuarter,
+        "Q": formatUnixTimestamp,
+        "s": formatUnixTimestampSeconds,
+        "S": formatSeconds,
+        "u": formatWeekdayNumberMonday,
+        "U": formatWeekNumberSunday,
+        "V": formatWeekNumberISO,
+        "w": formatWeekdayNumberSunday,
+        "W": formatWeekNumberMonday,
+        "x": null,
+        "X": null,
+        "y": formatYear,
+        "Y": formatFullYear,
+        "Z": formatZone,
+        "%": formatLiteralPercent
+      };
+    
+      var utcFormats = {
+        "a": formatUTCShortWeekday,
+        "A": formatUTCWeekday,
+        "b": formatUTCShortMonth,
+        "B": formatUTCMonth,
+        "c": null,
+        "d": formatUTCDayOfMonth,
+        "e": formatUTCDayOfMonth,
+        "f": formatUTCMicroseconds,
+        "g": formatUTCYearISO,
+        "G": formatUTCFullYearISO,
+        "H": formatUTCHour24,
+        "I": formatUTCHour12,
+        "j": formatUTCDayOfYear,
+        "L": formatUTCMilliseconds,
+        "m": formatUTCMonthNumber,
+        "M": formatUTCMinutes,
+        "p": formatUTCPeriod,
+        "q": formatUTCQuarter,
+        "Q": formatUnixTimestamp,
+        "s": formatUnixTimestampSeconds,
+        "S": formatUTCSeconds,
+        "u": formatUTCWeekdayNumberMonday,
+        "U": formatUTCWeekNumberSunday,
+        "V": formatUTCWeekNumberISO,
+        "w": formatUTCWeekdayNumberSunday,
+        "W": formatUTCWeekNumberMonday,
+        "x": null,
+        "X": null,
+        "y": formatUTCYear,
+        "Y": formatUTCFullYear,
+        "Z": formatUTCZone,
+        "%": formatLiteralPercent
+      };
+    
+      var parses = {
+        "a": parseShortWeekday,
+        "A": parseWeekday,
+        "b": parseShortMonth,
+        "B": parseMonth,
+        "c": parseLocaleDateTime,
+        "d": parseDayOfMonth,
+        "e": parseDayOfMonth,
+        "f": parseMicroseconds,
+        "g": parseYear,
+        "G": parseFullYear,
+        "H": parseHour24,
+        "I": parseHour24,
+        "j": parseDayOfYear,
+        "L": parseMilliseconds,
+        "m": parseMonthNumber,
+        "M": parseMinutes,
+        "p": parsePeriod,
+        "q": parseQuarter,
+        "Q": parseUnixTimestamp,
+        "s": parseUnixTimestampSeconds,
+        "S": parseSeconds,
+        "u": parseWeekdayNumberMonday,
+        "U": parseWeekNumberSunday,
+        "V": parseWeekNumberISO,
+        "w": parseWeekdayNumberSunday,
+        "W": parseWeekNumberMonday,
+        "x": parseLocaleDate,
+        "X": parseLocaleTime,
+        "y": parseYear,
+        "Y": parseFullYear,
+        "Z": parseZone,
+        "%": parseLiteralPercent
+      };
+    
+      // These recursive directive definitions must be deferred.
+      formats.x = newFormat(locale_date, formats);
+      formats.X = newFormat(locale_time, formats);
+      formats.c = newFormat(locale_dateTime, formats);
+      utcFormats.x = newFormat(locale_date, utcFormats);
+      utcFormats.X = newFormat(locale_time, utcFormats);
+      utcFormats.c = newFormat(locale_dateTime, utcFormats);
+    
+      function newFormat(specifier, formats) {
+        return function(date) {
+          var string = [],
+              i = -1,
+              j = 0,
+              n = specifier.length,
+              c,
+              pad,
+              format;
+    
+          if (!(date instanceof Date)) date = new Date(+date);
+    
+          while (++i < n) {
+            if (specifier.charCodeAt(i) === 37) {
+              string.push(specifier.slice(j, i));
+              if ((pad = pads[c = specifier.charAt(++i)]) != null) c = specifier.charAt(++i);
+              else pad = c === "e" ? " " : "0";
+              if (format = formats[c]) c = format(date, pad);
+              string.push(c);
+              j = i + 1;
+            }
+          }
+    
+          string.push(specifier.slice(j, i));
+          return string.join("");
+        };
+      }
+    
+      function newParse(specifier, Z) {
+        return function(string) {
+          var d = newDate(1900, undefined, 1),
+              i = parseSpecifier(d, specifier, string += "", 0),
+              week, day$1;
+          if (i != string.length) return null;
+    
+          // If a UNIX timestamp is specified, return it.
+          if ("Q" in d) return new Date(d.Q);
+          if ("s" in d) return new Date(d.s * 1000 + ("L" in d ? d.L : 0));
+    
+          // If this is utcParse, never use the local timezone.
+          if (Z && !("Z" in d)) d.Z = 0;
+    
+          // The am-pm flag is 0 for AM, and 1 for PM.
+          if ("p" in d) d.H = d.H % 12 + d.p * 12;
+    
+          // If the month was not specified, inherit from the quarter.
+          if (d.m === undefined) d.m = "q" in d ? d.q : 0;
+    
+          // Convert day-of-week and week-of-year to day-of-year.
+          if ("V" in d) {
+            if (d.V < 1 || d.V > 53) return null;
+            if (!("w" in d)) d.w = 1;
+            if ("Z" in d) {
+              week = utcDate(newDate(d.y, 0, 1)), day$1 = week.getUTCDay();
+              week = day$1 > 4 || day$1 === 0 ? utcMonday.ceil(week) : utcMonday(week);
+              week = utcDay.offset(week, (d.V - 1) * 7);
+              d.y = week.getUTCFullYear();
+              d.m = week.getUTCMonth();
+              d.d = week.getUTCDate() + (d.w + 6) % 7;
+            } else {
+              week = localDate(newDate(d.y, 0, 1)), day$1 = week.getDay();
+              week = day$1 > 4 || day$1 === 0 ? monday.ceil(week) : monday(week);
+              week = day.offset(week, (d.V - 1) * 7);
+              d.y = week.getFullYear();
+              d.m = week.getMonth();
+              d.d = week.getDate() + (d.w + 6) % 7;
+            }
+          } else if ("W" in d || "U" in d) {
+            if (!("w" in d)) d.w = "u" in d ? d.u % 7 : "W" in d ? 1 : 0;
+            day$1 = "Z" in d ? utcDate(newDate(d.y, 0, 1)).getUTCDay() : localDate(newDate(d.y, 0, 1)).getDay();
+            d.m = 0;
+            d.d = "W" in d ? (d.w + 6) % 7 + d.W * 7 - (day$1 + 5) % 7 : d.w + d.U * 7 - (day$1 + 6) % 7;
+          }
+    
+          // If a time zone is specified, all fields are interpreted as UTC and then
+          // offset according to the specified time zone.
+          if ("Z" in d) {
+            d.H += d.Z / 100 | 0;
+            d.M += d.Z % 100;
+            return utcDate(d);
+          }
+    
+          // Otherwise, all fields are in local time.
+          return localDate(d);
+        };
+      }
+    
+      function parseSpecifier(d, specifier, string, j) {
+        var i = 0,
+            n = specifier.length,
+            m = string.length,
+            c,
+            parse;
+    
+        while (i < n) {
+          if (j >= m) return -1;
+          c = specifier.charCodeAt(i++);
+          if (c === 37) {
+            c = specifier.charAt(i++);
+            parse = parses[c in pads ? specifier.charAt(i++) : c];
+            if (!parse || ((j = parse(d, string, j)) < 0)) return -1;
+          } else if (c != string.charCodeAt(j++)) {
+            return -1;
+          }
+        }
+    
+        return j;
+      }
+    
+      function parsePeriod(d, string, i) {
+        var n = periodRe.exec(string.slice(i));
+        return n ? (d.p = periodLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+      }
+    
+      function parseShortWeekday(d, string, i) {
+        var n = shortWeekdayRe.exec(string.slice(i));
+        return n ? (d.w = shortWeekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+      }
+    
+      function parseWeekday(d, string, i) {
+        var n = weekdayRe.exec(string.slice(i));
+        return n ? (d.w = weekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+      }
+    
+      function parseShortMonth(d, string, i) {
+        var n = shortMonthRe.exec(string.slice(i));
+        return n ? (d.m = shortMonthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+      }
+    
+      function parseMonth(d, string, i) {
+        var n = monthRe.exec(string.slice(i));
+        return n ? (d.m = monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;
+      }
+    
+      function parseLocaleDateTime(d, string, i) {
+        return parseSpecifier(d, locale_dateTime, string, i);
+      }
+    
+      function parseLocaleDate(d, string, i) {
+        return parseSpecifier(d, locale_date, string, i);
+      }
+    
+      function parseLocaleTime(d, string, i) {
+        return parseSpecifier(d, locale_time, string, i);
+      }
+    
+      function formatShortWeekday(d) {
+        return locale_shortWeekdays[d.getDay()];
+      }
+    
+      function formatWeekday(d) {
+        return locale_weekdays[d.getDay()];
+      }
+    
+      function formatShortMonth(d) {
+        return locale_shortMonths[d.getMonth()];
+      }
+    
+      function formatMonth(d) {
+        return locale_months[d.getMonth()];
+      }
+    
+      function formatPeriod(d) {
+        return locale_periods[+(d.getHours() >= 12)];
+      }
+    
+      function formatQuarter(d) {
+        return 1 + ~~(d.getMonth() / 3);
+      }
+    
+      function formatUTCShortWeekday(d) {
+        return locale_shortWeekdays[d.getUTCDay()];
+      }
+    
+      function formatUTCWeekday(d) {
+        return locale_weekdays[d.getUTCDay()];
+      }
+    
+      function formatUTCShortMonth(d) {
+        return locale_shortMonths[d.getUTCMonth()];
+      }
+    
+      function formatUTCMonth(d) {
+        return locale_months[d.getUTCMonth()];
+      }
+    
+      function formatUTCPeriod(d) {
+        return locale_periods[+(d.getUTCHours() >= 12)];
+      }
+    
+      function formatUTCQuarter(d) {
+        return 1 + ~~(d.getUTCMonth() / 3);
+      }
+    
+      return {
+        format: function(specifier) {
+          var f = newFormat(specifier += "", formats);
+          f.toString = function() { return specifier; };
+          return f;
+        },
+        parse: function(specifier) {
+          var p = newParse(specifier += "", false);
+          p.toString = function() { return specifier; };
+          return p;
+        },
+        utcFormat: function(specifier) {
+          var f = newFormat(specifier += "", utcFormats);
+          f.toString = function() { return specifier; };
+          return f;
+        },
+        utcParse: function(specifier) {
+          var p = newParse(specifier += "", true);
+          p.toString = function() { return specifier; };
+          return p;
+        }
+      };
+    }
+    
+    var pads = {"-": "", "_": " ", "0": "0"},
+        numberRe = /^\s*\d+/, // note: ignores next directive
+        percentRe = /^%/,
+        requoteRe = /[\\^$*+?|[\]().{}]/g;
+    
+    function pad(value, fill, width) {
+      var sign = value < 0 ? "-" : "",
+          string = (sign ? -value : value) + "",
+          length = string.length;
+      return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);
+    }
+    
+    function requote(s) {
+      return s.replace(requoteRe, "\\$&");
+    }
+    
+    function formatRe(names) {
+      return new RegExp("^(?:" + names.map(requote).join("|") + ")", "i");
+    }
+    
+    function formatLookup(names) {
+      return new Map(names.map((name, i) => [name.toLowerCase(), i]));
+    }
+    
+    function parseWeekdayNumberSunday(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 1));
+      return n ? (d.w = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseWeekdayNumberMonday(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 1));
+      return n ? (d.u = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseWeekNumberSunday(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.U = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseWeekNumberISO(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.V = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseWeekNumberMonday(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.W = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseFullYear(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 4));
+      return n ? (d.y = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseYear(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.y = +n[0] + (+n[0] > 68 ? 1900 : 2000), i + n[0].length) : -1;
+    }
+    
+    function parseZone(d, string, i) {
+      var n = /^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(string.slice(i, i + 6));
+      return n ? (d.Z = n[1] ? 0 : -(n[2] + (n[3] || "00")), i + n[0].length) : -1;
+    }
+    
+    function parseQuarter(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 1));
+      return n ? (d.q = n[0] * 3 - 3, i + n[0].length) : -1;
+    }
+    
+    function parseMonthNumber(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.m = n[0] - 1, i + n[0].length) : -1;
+    }
+    
+    function parseDayOfMonth(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.d = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseDayOfYear(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 3));
+      return n ? (d.m = 0, d.d = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseHour24(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.H = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseMinutes(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.M = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseSeconds(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 2));
+      return n ? (d.S = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseMilliseconds(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 3));
+      return n ? (d.L = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseMicroseconds(d, string, i) {
+      var n = numberRe.exec(string.slice(i, i + 6));
+      return n ? (d.L = Math.floor(n[0] / 1000), i + n[0].length) : -1;
+    }
+    
+    function parseLiteralPercent(d, string, i) {
+      var n = percentRe.exec(string.slice(i, i + 1));
+      return n ? i + n[0].length : -1;
+    }
+    
+    function parseUnixTimestamp(d, string, i) {
+      var n = numberRe.exec(string.slice(i));
+      return n ? (d.Q = +n[0], i + n[0].length) : -1;
+    }
+    
+    function parseUnixTimestampSeconds(d, string, i) {
+      var n = numberRe.exec(string.slice(i));
+      return n ? (d.s = +n[0], i + n[0].length) : -1;
+    }
+    
+    function formatDayOfMonth(d, p) {
+      return pad(d.getDate(), p, 2);
+    }
+    
+    function formatHour24(d, p) {
+      return pad(d.getHours(), p, 2);
+    }
+    
+    function formatHour12(d, p) {
+      return pad(d.getHours() % 12 || 12, p, 2);
+    }
+    
+    function formatDayOfYear(d, p) {
+      return pad(1 + day.count(year(d), d), p, 3);
+    }
+    
+    function formatMilliseconds(d, p) {
+      return pad(d.getMilliseconds(), p, 3);
+    }
+    
+    function formatMicroseconds(d, p) {
+      return formatMilliseconds(d, p) + "000";
+    }
+    
+    function formatMonthNumber(d, p) {
+      return pad(d.getMonth() + 1, p, 2);
+    }
+    
+    function formatMinutes(d, p) {
+      return pad(d.getMinutes(), p, 2);
+    }
+    
+    function formatSeconds(d, p) {
+      return pad(d.getSeconds(), p, 2);
+    }
+    
+    function formatWeekdayNumberMonday(d) {
+      var day = d.getDay();
+      return day === 0 ? 7 : day;
+    }
+    
+    function formatWeekNumberSunday(d, p) {
+      return pad(sunday.count(year(d) - 1, d), p, 2);
+    }
+    
+    function dISO(d) {
+      var day = d.getDay();
+      return (day >= 4 || day === 0) ? thursday(d) : thursday.ceil(d);
+    }
+    
+    function formatWeekNumberISO(d, p) {
+      d = dISO(d);
+      return pad(thursday.count(year(d), d) + (year(d).getDay() === 4), p, 2);
+    }
+    
+    function formatWeekdayNumberSunday(d) {
+      return d.getDay();
+    }
+    
+    function formatWeekNumberMonday(d, p) {
+      return pad(monday.count(year(d) - 1, d), p, 2);
+    }
+    
+    function formatYear(d, p) {
+      return pad(d.getFullYear() % 100, p, 2);
+    }
+    
+    function formatYearISO(d, p) {
+      d = dISO(d);
+      return pad(d.getFullYear() % 100, p, 2);
+    }
+    
+    function formatFullYear(d, p) {
+      return pad(d.getFullYear() % 10000, p, 4);
+    }
+    
+    function formatFullYearISO(d, p) {
+      var day = d.getDay();
+      d = (day >= 4 || day === 0) ? thursday(d) : thursday.ceil(d);
+      return pad(d.getFullYear() % 10000, p, 4);
+    }
+    
+    function formatZone(d) {
+      var z = d.getTimezoneOffset();
+      return (z > 0 ? "-" : (z *= -1, "+"))
+          + pad(z / 60 | 0, "0", 2)
+          + pad(z % 60, "0", 2);
+    }
+    
+    function formatUTCDayOfMonth(d, p) {
+      return pad(d.getUTCDate(), p, 2);
+    }
+    
+    function formatUTCHour24(d, p) {
+      return pad(d.getUTCHours(), p, 2);
+    }
+    
+    function formatUTCHour12(d, p) {
+      return pad(d.getUTCHours() % 12 || 12, p, 2);
+    }
+    
+    function formatUTCDayOfYear(d, p) {
+      return pad(1 + utcDay.count(utcYear(d), d), p, 3);
+    }
+    
+    function formatUTCMilliseconds(d, p) {
+      return pad(d.getUTCMilliseconds(), p, 3);
+    }
+    
+    function formatUTCMicroseconds(d, p) {
+      return formatUTCMilliseconds(d, p) + "000";
+    }
+    
+    function formatUTCMonthNumber(d, p) {
+      return pad(d.getUTCMonth() + 1, p, 2);
+    }
+    
+    function formatUTCMinutes(d, p) {
+      return pad(d.getUTCMinutes(), p, 2);
+    }
+    
+    function formatUTCSeconds(d, p) {
+      return pad(d.getUTCSeconds(), p, 2);
+    }
+    
+    function formatUTCWeekdayNumberMonday(d) {
+      var dow = d.getUTCDay();
+      return dow === 0 ? 7 : dow;
+    }
+    
+    function formatUTCWeekNumberSunday(d, p) {
+      return pad(utcSunday.count(utcYear(d) - 1, d), p, 2);
+    }
+    
+    function UTCdISO(d) {
+      var day = d.getUTCDay();
+      return (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);
+    }
+    
+    function formatUTCWeekNumberISO(d, p) {
+      d = UTCdISO(d);
+      return pad(utcThursday.count(utcYear(d), d) + (utcYear(d).getUTCDay() === 4), p, 2);
+    }
+    
+    function formatUTCWeekdayNumberSunday(d) {
+      return d.getUTCDay();
+    }
+    
+    function formatUTCWeekNumberMonday(d, p) {
+      return pad(utcMonday.count(utcYear(d) - 1, d), p, 2);
+    }
+    
+    function formatUTCYear(d, p) {
+      return pad(d.getUTCFullYear() % 100, p, 2);
+    }
+    
+    function formatUTCYearISO(d, p) {
+      d = UTCdISO(d);
+      return pad(d.getUTCFullYear() % 100, p, 2);
+    }
+    
+    function formatUTCFullYear(d, p) {
+      return pad(d.getUTCFullYear() % 10000, p, 4);
+    }
+    
+    function formatUTCFullYearISO(d, p) {
+      var day = d.getUTCDay();
+      d = (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);
+      return pad(d.getUTCFullYear() % 10000, p, 4);
+    }
+    
+    function formatUTCZone() {
+      return "+0000";
+    }
+    
+    function formatLiteralPercent() {
+      return "%";
+    }
+    
+    function formatUnixTimestamp(d) {
+      return +d;
+    }
+    
+    function formatUnixTimestampSeconds(d) {
+      return Math.floor(+d / 1000);
+    }
+    
+    var locale;
+    exports.timeFormat = void 0;
+    exports.timeParse = void 0;
+    exports.utcFormat = void 0;
+    exports.utcParse = void 0;
+    
+    defaultLocale({
+      dateTime: "%x, %X",
+      date: "%-m/%-d/%Y",
+      time: "%-I:%M:%S %p",
+      periods: ["AM", "PM"],
+      days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+      shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+      months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+      shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+    });
+    
+    function defaultLocale(definition) {
+      locale = formatLocale(definition);
+      exports.timeFormat = locale.format;
+      exports.timeParse = locale.parse;
+      exports.utcFormat = locale.utcFormat;
+      exports.utcParse = locale.utcParse;
+      return locale;
+    }
+    
+    var isoSpecifier = "%Y-%m-%dT%H:%M:%S.%LZ";
+    
+    function formatIsoNative(date) {
+      return date.toISOString();
+    }
+    
+    var formatIso = Date.prototype.toISOString
+        ? formatIsoNative
+        : exports.utcFormat(isoSpecifier);
+    
+    function parseIsoNative(string) {
+      var date = new Date(string);
+      return isNaN(date) ? null : date;
+    }
+    
+    var parseIso = +new Date("2000-01-01T00:00:00.000Z")
+        ? parseIsoNative
+        : exports.utcParse(isoSpecifier);
+    
+    function date(t) {
+      return new Date(t);
+    }
+    
+    function number(t) {
+      return t instanceof Date ? +t : +new Date(+t);
+    }
+    
+    function calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format) {
+      var scale = continuous(),
+          invert = scale.invert,
+          domain = scale.domain;
+    
+      var formatMillisecond = format(".%L"),
+          formatSecond = format(":%S"),
+          formatMinute = format("%I:%M"),
+          formatHour = format("%I %p"),
+          formatDay = format("%a %d"),
+          formatWeek = format("%b %d"),
+          formatMonth = format("%B"),
+          formatYear = format("%Y");
+    
+      function tickFormat(date) {
+        return (second(date) < date ? formatMillisecond
+            : minute(date) < date ? formatSecond
+            : hour(date) < date ? formatMinute
+            : day(date) < date ? formatHour
+            : month(date) < date ? (week(date) < date ? formatDay : formatWeek)
+            : year(date) < date ? formatMonth
+            : formatYear)(date);
+      }
+    
+      scale.invert = function(y) {
+        return new Date(invert(y));
+      };
+    
+      scale.domain = function(_) {
+        return arguments.length ? domain(Array.from(_, number)) : domain().map(date);
+      };
+    
+      scale.ticks = function(interval) {
+        var d = domain();
+        return ticks(d[0], d[d.length - 1], interval == null ? 10 : interval);
+      };
+    
+      scale.tickFormat = function(count, specifier) {
+        return specifier == null ? tickFormat : format(specifier);
+      };
+    
+      scale.nice = function(interval) {
+        var d = domain();
+        if (!interval || typeof interval.range !== "function") interval = tickInterval(d[0], d[d.length - 1], interval == null ? 10 : interval);
+        return interval ? domain(nice(d, interval)) : scale;
+      };
+    
+      scale.copy = function() {
+        return copy$1(scale, calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format));
+      };
+    
+      return scale;
+    }
+    
+    function time() {
+      return initRange.apply(calendar(timeTicks, timeTickInterval, year, month, sunday, day, hour, minute, second, exports.timeFormat).domain([new Date(2000, 0, 1), new Date(2000, 0, 2)]), arguments);
+    }
+    
+    function utcTime() {
+      return initRange.apply(calendar(utcTicks, utcTickInterval, utcYear, utcMonth, utcSunday, utcDay, utcHour, utcMinute, second, exports.utcFormat).domain([Date.UTC(2000, 0, 1), Date.UTC(2000, 0, 2)]), arguments);
+    }
+    
+    function transformer$1() {
+      var x0 = 0,
+          x1 = 1,
+          t0,
+          t1,
+          k10,
+          transform,
+          interpolator = identity$3,
+          clamp = false,
+          unknown;
+    
+      function scale(x) {
+        return x == null || isNaN(x = +x) ? unknown : interpolator(k10 === 0 ? 0.5 : (x = (transform(x) - t0) * k10, clamp ? Math.max(0, Math.min(1, x)) : x));
+      }
+    
+      scale.domain = function(_) {
+        return arguments.length ? ([x0, x1] = _, t0 = transform(x0 = +x0), t1 = transform(x1 = +x1), k10 = t0 === t1 ? 0 : 1 / (t1 - t0), scale) : [x0, x1];
+      };
+    
+      scale.clamp = function(_) {
+        return arguments.length ? (clamp = !!_, scale) : clamp;
+      };
+    
+      scale.interpolator = function(_) {
+        return arguments.length ? (interpolator = _, scale) : interpolator;
+      };
+    
+      function range(interpolate) {
+        return function(_) {
+          var r0, r1;
+          return arguments.length ? ([r0, r1] = _, interpolator = interpolate(r0, r1), scale) : [interpolator(0), interpolator(1)];
+        };
+      }
+    
+      scale.range = range(interpolate$2);
+    
+      scale.rangeRound = range(interpolateRound);
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      return function(t) {
+        transform = t, t0 = t(x0), t1 = t(x1), k10 = t0 === t1 ? 0 : 1 / (t1 - t0);
+        return scale;
+      };
+    }
+    
+    function copy(source, target) {
+      return target
+          .domain(source.domain())
+          .interpolator(source.interpolator())
+          .clamp(source.clamp())
+          .unknown(source.unknown());
+    }
+    
+    function sequential() {
+      var scale = linearish(transformer$1()(identity$3));
+    
+      scale.copy = function() {
+        return copy(scale, sequential());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function sequentialLog() {
+      var scale = loggish(transformer$1()).domain([1, 10]);
+    
+      scale.copy = function() {
+        return copy(scale, sequentialLog()).base(scale.base());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function sequentialSymlog() {
+      var scale = symlogish(transformer$1());
+    
+      scale.copy = function() {
+        return copy(scale, sequentialSymlog()).constant(scale.constant());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function sequentialPow() {
+      var scale = powish(transformer$1());
+    
+      scale.copy = function() {
+        return copy(scale, sequentialPow()).exponent(scale.exponent());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function sequentialSqrt() {
+      return sequentialPow.apply(null, arguments).exponent(0.5);
+    }
+    
+    function sequentialQuantile() {
+      var domain = [],
+          interpolator = identity$3;
+    
+      function scale(x) {
+        if (x != null && !isNaN(x = +x)) return interpolator((bisectRight(domain, x, 1) - 1) / (domain.length - 1));
+      }
+    
+      scale.domain = function(_) {
+        if (!arguments.length) return domain.slice();
+        domain = [];
+        for (let d of _) if (d != null && !isNaN(d = +d)) domain.push(d);
+        domain.sort(ascending$3);
+        return scale;
+      };
+    
+      scale.interpolator = function(_) {
+        return arguments.length ? (interpolator = _, scale) : interpolator;
+      };
+    
+      scale.range = function() {
+        return domain.map((d, i) => interpolator(i / (domain.length - 1)));
+      };
+    
+      scale.quantiles = function(n) {
+        return Array.from({length: n + 1}, (_, i) => quantile$1(domain, i / n));
+      };
+    
+      scale.copy = function() {
+        return sequentialQuantile(interpolator).domain(domain);
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function transformer() {
+      var x0 = 0,
+          x1 = 0.5,
+          x2 = 1,
+          s = 1,
+          t0,
+          t1,
+          t2,
+          k10,
+          k21,
+          interpolator = identity$3,
+          transform,
+          clamp = false,
+          unknown;
+    
+      function scale(x) {
+        return isNaN(x = +x) ? unknown : (x = 0.5 + ((x = +transform(x)) - t1) * (s * x < s * t1 ? k10 : k21), interpolator(clamp ? Math.max(0, Math.min(1, x)) : x));
+      }
+    
+      scale.domain = function(_) {
+        return arguments.length ? ([x0, x1, x2] = _, t0 = transform(x0 = +x0), t1 = transform(x1 = +x1), t2 = transform(x2 = +x2), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1), s = t1 < t0 ? -1 : 1, scale) : [x0, x1, x2];
+      };
+    
+      scale.clamp = function(_) {
+        return arguments.length ? (clamp = !!_, scale) : clamp;
+      };
+    
+      scale.interpolator = function(_) {
+        return arguments.length ? (interpolator = _, scale) : interpolator;
+      };
+    
+      function range(interpolate) {
+        return function(_) {
+          var r0, r1, r2;
+          return arguments.length ? ([r0, r1, r2] = _, interpolator = piecewise(interpolate, [r0, r1, r2]), scale) : [interpolator(0), interpolator(0.5), interpolator(1)];
+        };
+      }
+    
+      scale.range = range(interpolate$2);
+    
+      scale.rangeRound = range(interpolateRound);
+    
+      scale.unknown = function(_) {
+        return arguments.length ? (unknown = _, scale) : unknown;
+      };
+    
+      return function(t) {
+        transform = t, t0 = t(x0), t1 = t(x1), t2 = t(x2), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1), s = t1 < t0 ? -1 : 1;
+        return scale;
+      };
+    }
+    
+    function diverging$1() {
+      var scale = linearish(transformer()(identity$3));
+    
+      scale.copy = function() {
+        return copy(scale, diverging$1());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function divergingLog() {
+      var scale = loggish(transformer()).domain([0.1, 1, 10]);
+    
+      scale.copy = function() {
+        return copy(scale, divergingLog()).base(scale.base());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function divergingSymlog() {
+      var scale = symlogish(transformer());
+    
+      scale.copy = function() {
+        return copy(scale, divergingSymlog()).constant(scale.constant());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function divergingPow() {
+      var scale = powish(transformer());
+    
+      scale.copy = function() {
+        return copy(scale, divergingPow()).exponent(scale.exponent());
+      };
+    
+      return initInterpolator.apply(scale, arguments);
+    }
+    
+    function divergingSqrt() {
+      return divergingPow.apply(null, arguments).exponent(0.5);
+    }
+    
+    function colors(specifier) {
+      var n = specifier.length / 6 | 0, colors = new Array(n), i = 0;
+      while (i < n) colors[i] = "#" + specifier.slice(i * 6, ++i * 6);
+      return colors;
+    }
+    
+    var category10 = colors("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf");
+    
+    var Accent = colors("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666");
+    
+    var Dark2 = colors("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666");
+    
+    var Paired = colors("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928");
+    
+    var Pastel1 = colors("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2");
+    
+    var Pastel2 = colors("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc");
+    
+    var Set1 = colors("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999");
+    
+    var Set2 = colors("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3");
+    
+    var Set3 = colors("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f");
+    
+    var Tableau10 = colors("4e79a7f28e2ce1575976b7b259a14fedc949af7aa1ff9da79c755fbab0ab");
+    
+    var ramp$1 = scheme => rgbBasis(scheme[scheme.length - 1]);
+    
+    var scheme$q = new Array(3).concat(
+      "d8b365f5f5f55ab4ac",
+      "a6611adfc27d80cdc1018571",
+      "a6611adfc27df5f5f580cdc1018571",
+      "8c510ad8b365f6e8c3c7eae55ab4ac01665e",
+      "8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e",
+      "8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e",
+      "8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e",
+      "5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30",
+      "5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30"
+    ).map(colors);
+    
+    var BrBG = ramp$1(scheme$q);
+    
+    var scheme$p = new Array(3).concat(
+      "af8dc3f7f7f77fbf7b",
+      "7b3294c2a5cfa6dba0008837",
+      "7b3294c2a5cff7f7f7a6dba0008837",
+      "762a83af8dc3e7d4e8d9f0d37fbf7b1b7837",
+      "762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837",
+      "762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837",
+      "762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837",
+      "40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b",
+      "40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b"
+    ).map(colors);
+    
+    var PRGn = ramp$1(scheme$p);
+    
+    var scheme$o = new Array(3).concat(
+      "e9a3c9f7f7f7a1d76a",
+      "d01c8bf1b6dab8e1864dac26",
+      "d01c8bf1b6daf7f7f7b8e1864dac26",
+      "c51b7de9a3c9fde0efe6f5d0a1d76a4d9221",
+      "c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221",
+      "c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221",
+      "c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221",
+      "8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419",
+      "8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419"
+    ).map(colors);
+    
+    var PiYG = ramp$1(scheme$o);
+    
+    var scheme$n = new Array(3).concat(
+      "998ec3f7f7f7f1a340",
+      "5e3c99b2abd2fdb863e66101",
+      "5e3c99b2abd2f7f7f7fdb863e66101",
+      "542788998ec3d8daebfee0b6f1a340b35806",
+      "542788998ec3d8daebf7f7f7fee0b6f1a340b35806",
+      "5427888073acb2abd2d8daebfee0b6fdb863e08214b35806",
+      "5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806",
+      "2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08",
+      "2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08"
+    ).map(colors);
+    
+    var PuOr = ramp$1(scheme$n);
+    
+    var scheme$m = new Array(3).concat(
+      "ef8a62f7f7f767a9cf",
+      "ca0020f4a58292c5de0571b0",
+      "ca0020f4a582f7f7f792c5de0571b0",
+      "b2182bef8a62fddbc7d1e5f067a9cf2166ac",
+      "b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac",
+      "b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac",
+      "b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac",
+      "67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061",
+      "67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061"
+    ).map(colors);
+    
+    var RdBu = ramp$1(scheme$m);
+    
+    var scheme$l = new Array(3).concat(
+      "ef8a62ffffff999999",
+      "ca0020f4a582bababa404040",
+      "ca0020f4a582ffffffbababa404040",
+      "b2182bef8a62fddbc7e0e0e09999994d4d4d",
+      "b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d",
+      "b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d",
+      "b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d",
+      "67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a",
+      "67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a"
+    ).map(colors);
+    
+    var RdGy = ramp$1(scheme$l);
+    
+    var scheme$k = new Array(3).concat(
+      "fc8d59ffffbf91bfdb",
+      "d7191cfdae61abd9e92c7bb6",
+      "d7191cfdae61ffffbfabd9e92c7bb6",
+      "d73027fc8d59fee090e0f3f891bfdb4575b4",
+      "d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4",
+      "d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4",
+      "d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4",
+      "a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695",
+      "a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695"
+    ).map(colors);
+    
+    var RdYlBu = ramp$1(scheme$k);
+    
+    var scheme$j = new Array(3).concat(
+      "fc8d59ffffbf91cf60",
+      "d7191cfdae61a6d96a1a9641",
+      "d7191cfdae61ffffbfa6d96a1a9641",
+      "d73027fc8d59fee08bd9ef8b91cf601a9850",
+      "d73027fc8d59fee08bffffbfd9ef8b91cf601a9850",
+      "d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850",
+      "d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850",
+      "a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837",
+      "a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837"
+    ).map(colors);
+    
+    var RdYlGn = ramp$1(scheme$j);
+    
+    var scheme$i = new Array(3).concat(
+      "fc8d59ffffbf99d594",
+      "d7191cfdae61abdda42b83ba",
+      "d7191cfdae61ffffbfabdda42b83ba",
+      "d53e4ffc8d59fee08be6f59899d5943288bd",
+      "d53e4ffc8d59fee08bffffbfe6f59899d5943288bd",
+      "d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd",
+      "d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd",
+      "9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2",
+      "9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2"
+    ).map(colors);
+    
+    var Spectral = ramp$1(scheme$i);
+    
+    var scheme$h = new Array(3).concat(
+      "e5f5f999d8c92ca25f",
+      "edf8fbb2e2e266c2a4238b45",
+      "edf8fbb2e2e266c2a42ca25f006d2c",
+      "edf8fbccece699d8c966c2a42ca25f006d2c",
+      "edf8fbccece699d8c966c2a441ae76238b45005824",
+      "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824",
+      "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b"
+    ).map(colors);
+    
+    var BuGn = ramp$1(scheme$h);
+    
+    var scheme$g = new Array(3).concat(
+      "e0ecf49ebcda8856a7",
+      "edf8fbb3cde38c96c688419d",
+      "edf8fbb3cde38c96c68856a7810f7c",
+      "edf8fbbfd3e69ebcda8c96c68856a7810f7c",
+      "edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b",
+      "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b",
+      "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b"
+    ).map(colors);
+    
+    var BuPu = ramp$1(scheme$g);
+    
+    var scheme$f = new Array(3).concat(
+      "e0f3dba8ddb543a2ca",
+      "f0f9e8bae4bc7bccc42b8cbe",
+      "f0f9e8bae4bc7bccc443a2ca0868ac",
+      "f0f9e8ccebc5a8ddb57bccc443a2ca0868ac",
+      "f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e",
+      "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e",
+      "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081"
+    ).map(colors);
+    
+    var GnBu = ramp$1(scheme$f);
+    
+    var scheme$e = new Array(3).concat(
+      "fee8c8fdbb84e34a33",
+      "fef0d9fdcc8afc8d59d7301f",
+      "fef0d9fdcc8afc8d59e34a33b30000",
+      "fef0d9fdd49efdbb84fc8d59e34a33b30000",
+      "fef0d9fdd49efdbb84fc8d59ef6548d7301f990000",
+      "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000",
+      "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000"
+    ).map(colors);
+    
+    var OrRd = ramp$1(scheme$e);
+    
+    var scheme$d = new Array(3).concat(
+      "ece2f0a6bddb1c9099",
+      "f6eff7bdc9e167a9cf02818a",
+      "f6eff7bdc9e167a9cf1c9099016c59",
+      "f6eff7d0d1e6a6bddb67a9cf1c9099016c59",
+      "f6eff7d0d1e6a6bddb67a9cf3690c002818a016450",
+      "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450",
+      "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636"
+    ).map(colors);
+    
+    var PuBuGn = ramp$1(scheme$d);
+    
+    var scheme$c = new Array(3).concat(
+      "ece7f2a6bddb2b8cbe",
+      "f1eef6bdc9e174a9cf0570b0",
+      "f1eef6bdc9e174a9cf2b8cbe045a8d",
+      "f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d",
+      "f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b",
+      "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b",
+      "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858"
+    ).map(colors);
+    
+    var PuBu = ramp$1(scheme$c);
+    
+    var scheme$b = new Array(3).concat(
+      "e7e1efc994c7dd1c77",
+      "f1eef6d7b5d8df65b0ce1256",
+      "f1eef6d7b5d8df65b0dd1c77980043",
+      "f1eef6d4b9dac994c7df65b0dd1c77980043",
+      "f1eef6d4b9dac994c7df65b0e7298ace125691003f",
+      "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f",
+      "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f"
+    ).map(colors);
+    
+    var PuRd = ramp$1(scheme$b);
+    
+    var scheme$a = new Array(3).concat(
+      "fde0ddfa9fb5c51b8a",
+      "feebe2fbb4b9f768a1ae017e",
+      "feebe2fbb4b9f768a1c51b8a7a0177",
+      "feebe2fcc5c0fa9fb5f768a1c51b8a7a0177",
+      "feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177",
+      "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177",
+      "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a"
+    ).map(colors);
+    
+    var RdPu = ramp$1(scheme$a);
+    
+    var scheme$9 = new Array(3).concat(
+      "edf8b17fcdbb2c7fb8",
+      "ffffcca1dab441b6c4225ea8",
+      "ffffcca1dab441b6c42c7fb8253494",
+      "ffffccc7e9b47fcdbb41b6c42c7fb8253494",
+      "ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84",
+      "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84",
+      "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58"
+    ).map(colors);
+    
+    var YlGnBu = ramp$1(scheme$9);
+    
+    var scheme$8 = new Array(3).concat(
+      "f7fcb9addd8e31a354",
+      "ffffccc2e69978c679238443",
+      "ffffccc2e69978c67931a354006837",
+      "ffffccd9f0a3addd8e78c67931a354006837",
+      "ffffccd9f0a3addd8e78c67941ab5d238443005a32",
+      "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32",
+      "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529"
+    ).map(colors);
+    
+    var YlGn = ramp$1(scheme$8);
+    
+    var scheme$7 = new Array(3).concat(
+      "fff7bcfec44fd95f0e",
+      "ffffd4fed98efe9929cc4c02",
+      "ffffd4fed98efe9929d95f0e993404",
+      "ffffd4fee391fec44ffe9929d95f0e993404",
+      "ffffd4fee391fec44ffe9929ec7014cc4c028c2d04",
+      "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04",
+      "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506"
+    ).map(colors);
+    
+    var YlOrBr = ramp$1(scheme$7);
+    
+    var scheme$6 = new Array(3).concat(
+      "ffeda0feb24cf03b20",
+      "ffffb2fecc5cfd8d3ce31a1c",
+      "ffffb2fecc5cfd8d3cf03b20bd0026",
+      "ffffb2fed976feb24cfd8d3cf03b20bd0026",
+      "ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026",
+      "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026",
+      "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026"
+    ).map(colors);
+    
+    var YlOrRd = ramp$1(scheme$6);
+    
+    var scheme$5 = new Array(3).concat(
+      "deebf79ecae13182bd",
+      "eff3ffbdd7e76baed62171b5",
+      "eff3ffbdd7e76baed63182bd08519c",
+      "eff3ffc6dbef9ecae16baed63182bd08519c",
+      "eff3ffc6dbef9ecae16baed64292c62171b5084594",
+      "f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594",
+      "f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b"
+    ).map(colors);
+    
+    var Blues = ramp$1(scheme$5);
+    
+    var scheme$4 = new Array(3).concat(
+      "e5f5e0a1d99b31a354",
+      "edf8e9bae4b374c476238b45",
+      "edf8e9bae4b374c47631a354006d2c",
+      "edf8e9c7e9c0a1d99b74c47631a354006d2c",
+      "edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32",
+      "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32",
+      "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b"
+    ).map(colors);
+    
+    var Greens = ramp$1(scheme$4);
+    
+    var scheme$3 = new Array(3).concat(
+      "f0f0f0bdbdbd636363",
+      "f7f7f7cccccc969696525252",
+      "f7f7f7cccccc969696636363252525",
+      "f7f7f7d9d9d9bdbdbd969696636363252525",
+      "f7f7f7d9d9d9bdbdbd969696737373525252252525",
+      "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525",
+      "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000"
+    ).map(colors);
+    
+    var Greys = ramp$1(scheme$3);
+    
+    var scheme$2 = new Array(3).concat(
+      "efedf5bcbddc756bb1",
+      "f2f0f7cbc9e29e9ac86a51a3",
+      "f2f0f7cbc9e29e9ac8756bb154278f",
+      "f2f0f7dadaebbcbddc9e9ac8756bb154278f",
+      "f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486",
+      "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486",
+      "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d"
+    ).map(colors);
+    
+    var Purples = ramp$1(scheme$2);
+    
+    var scheme$1 = new Array(3).concat(
+      "fee0d2fc9272de2d26",
+      "fee5d9fcae91fb6a4acb181d",
+      "fee5d9fcae91fb6a4ade2d26a50f15",
+      "fee5d9fcbba1fc9272fb6a4ade2d26a50f15",
+      "fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d",
+      "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d",
+      "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d"
+    ).map(colors);
+    
+    var Reds = ramp$1(scheme$1);
+    
+    var scheme = new Array(3).concat(
+      "fee6cefdae6be6550d",
+      "feeddefdbe85fd8d3cd94701",
+      "feeddefdbe85fd8d3ce6550da63603",
+      "feeddefdd0a2fdae6bfd8d3ce6550da63603",
+      "feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04",
+      "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04",
+      "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704"
+    ).map(colors);
+    
+    var Oranges = ramp$1(scheme);
+    
+    function cividis(t) {
+      t = Math.max(0, Math.min(1, t));
+      return "rgb("
+          + Math.max(0, Math.min(255, Math.round(-4.54 - t * (35.34 - t * (2381.73 - t * (6402.7 - t * (7024.72 - t * 2710.57))))))) + ", "
+          + Math.max(0, Math.min(255, Math.round(32.49 + t * (170.73 + t * (52.82 - t * (131.46 - t * (176.58 - t * 67.37))))))) + ", "
+          + Math.max(0, Math.min(255, Math.round(81.24 + t * (442.36 - t * (2482.43 - t * (6167.24 - t * (6614.94 - t * 2475.67)))))))
+          + ")";
+    }
+    
+    var cubehelix = cubehelixLong(cubehelix$3(300, 0.5, 0.0), cubehelix$3(-240, 0.5, 1.0));
+    
+    var warm = cubehelixLong(cubehelix$3(-100, 0.75, 0.35), cubehelix$3(80, 1.50, 0.8));
+    
+    var cool = cubehelixLong(cubehelix$3(260, 0.75, 0.35), cubehelix$3(80, 1.50, 0.8));
+    
+    var c$2 = cubehelix$3();
+    
+    function rainbow(t) {
+      if (t < 0 || t > 1) t -= Math.floor(t);
+      var ts = Math.abs(t - 0.5);
+      c$2.h = 360 * t - 100;
+      c$2.s = 1.5 - 1.5 * ts;
+      c$2.l = 0.8 - 0.9 * ts;
+      return c$2 + "";
+    }
+    
+    var c$1 = rgb(),
+        pi_1_3 = Math.PI / 3,
+        pi_2_3 = Math.PI * 2 / 3;
+    
+    function sinebow(t) {
+      var x;
+      t = (0.5 - t) * Math.PI;
+      c$1.r = 255 * (x = Math.sin(t)) * x;
+      c$1.g = 255 * (x = Math.sin(t + pi_1_3)) * x;
+      c$1.b = 255 * (x = Math.sin(t + pi_2_3)) * x;
+      return c$1 + "";
+    }
+    
+    function turbo(t) {
+      t = Math.max(0, Math.min(1, t));
+      return "rgb("
+          + Math.max(0, Math.min(255, Math.round(34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05))))))) + ", "
+          + Math.max(0, Math.min(255, Math.round(23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56))))))) + ", "
+          + Math.max(0, Math.min(255, Math.round(27.2 + t * (3211.1 - t * (15327.97 - t * (27814 - t * (22569.18 - t * 6838.66)))))))
+          + ")";
+    }
+    
+    function ramp(range) {
+      var n = range.length;
+      return function(t) {
+        return range[Math.max(0, Math.min(n - 1, Math.floor(t * n)))];
+      };
+    }
+    
+    var viridis = ramp(colors("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));
+    
+    var magma = ramp(colors("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf"));
+    
+    var inferno = ramp(colors("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4"));
+    
+    var plasma = ramp(colors("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));
+    
+    function constant$1(x) {
+      return function constant() {
+        return x;
+      };
+    }
+    
+    var abs = Math.abs;
+    var atan2 = Math.atan2;
+    var cos = Math.cos;
+    var max = Math.max;
+    var min = Math.min;
+    var sin = Math.sin;
+    var sqrt = Math.sqrt;
+    
+    var epsilon = 1e-12;
+    var pi = Math.PI;
+    var halfPi = pi / 2;
+    var tau = 2 * pi;
+    
+    function acos(x) {
+      return x > 1 ? 0 : x < -1 ? pi : Math.acos(x);
+    }
+    
+    function asin(x) {
+      return x >= 1 ? halfPi : x <= -1 ? -halfPi : Math.asin(x);
+    }
+    
+    function arcInnerRadius(d) {
+      return d.innerRadius;
+    }
+    
+    function arcOuterRadius(d) {
+      return d.outerRadius;
+    }
+    
+    function arcStartAngle(d) {
+      return d.startAngle;
+    }
+    
+    function arcEndAngle(d) {
+      return d.endAngle;
+    }
+    
+    function arcPadAngle(d) {
+      return d && d.padAngle; // Note: optional!
+    }
+    
+    function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
+      var x10 = x1 - x0, y10 = y1 - y0,
+          x32 = x3 - x2, y32 = y3 - y2,
+          t = y32 * x10 - x32 * y10;
+      if (t * t < epsilon) return;
+      t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
+      return [x0 + t * x10, y0 + t * y10];
+    }
+    
+    // Compute perpendicular offset line of length rc.
+    // http://mathworld.wolfram.com/Circle-LineIntersection.html
+    function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
+      var x01 = x0 - x1,
+          y01 = y0 - y1,
+          lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01),
+          ox = lo * y01,
+          oy = -lo * x01,
+          x11 = x0 + ox,
+          y11 = y0 + oy,
+          x10 = x1 + ox,
+          y10 = y1 + oy,
+          x00 = (x11 + x10) / 2,
+          y00 = (y11 + y10) / 2,
+          dx = x10 - x11,
+          dy = y10 - y11,
+          d2 = dx * dx + dy * dy,
+          r = r1 - rc,
+          D = x11 * y10 - x10 * y11,
+          d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)),
+          cx0 = (D * dy - dx * d) / d2,
+          cy0 = (-D * dx - dy * d) / d2,
+          cx1 = (D * dy + dx * d) / d2,
+          cy1 = (-D * dx + dy * d) / d2,
+          dx0 = cx0 - x00,
+          dy0 = cy0 - y00,
+          dx1 = cx1 - x00,
+          dy1 = cy1 - y00;
+    
+      // Pick the closer of the two intersection points.
+      // TODO Is there a faster way to determine which intersection to use?
+      if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
+    
+      return {
+        cx: cx0,
+        cy: cy0,
+        x01: -ox,
+        y01: -oy,
+        x11: cx0 * (r1 / r - 1),
+        y11: cy0 * (r1 / r - 1)
+      };
+    }
+    
+    function arc() {
+      var innerRadius = arcInnerRadius,
+          outerRadius = arcOuterRadius,
+          cornerRadius = constant$1(0),
+          padRadius = null,
+          startAngle = arcStartAngle,
+          endAngle = arcEndAngle,
+          padAngle = arcPadAngle,
+          context = null;
+    
+      function arc() {
+        var buffer,
+            r,
+            r0 = +innerRadius.apply(this, arguments),
+            r1 = +outerRadius.apply(this, arguments),
+            a0 = startAngle.apply(this, arguments) - halfPi,
+            a1 = endAngle.apply(this, arguments) - halfPi,
+            da = abs(a1 - a0),
+            cw = a1 > a0;
+    
+        if (!context) context = buffer = path();
+    
+        // Ensure that the outer radius is always larger than the inner radius.
+        if (r1 < r0) r = r1, r1 = r0, r0 = r;
+    
+        // Is it a point?
+        if (!(r1 > epsilon)) context.moveTo(0, 0);
+    
+        // Or is it a circle or annulus?
+        else if (da > tau - epsilon) {
+          context.moveTo(r1 * cos(a0), r1 * sin(a0));
+          context.arc(0, 0, r1, a0, a1, !cw);
+          if (r0 > epsilon) {
+            context.moveTo(r0 * cos(a1), r0 * sin(a1));
+            context.arc(0, 0, r0, a1, a0, cw);
+          }
+        }
+    
+        // Or is it a circular or annular sector?
+        else {
+          var a01 = a0,
+              a11 = a1,
+              a00 = a0,
+              a10 = a1,
+              da0 = da,
+              da1 = da,
+              ap = padAngle.apply(this, arguments) / 2,
+              rp = (ap > epsilon) && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)),
+              rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
+              rc0 = rc,
+              rc1 = rc,
+              t0,
+              t1;
+    
+          // Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
+          if (rp > epsilon) {
+            var p0 = asin(rp / r0 * sin(ap)),
+                p1 = asin(rp / r1 * sin(ap));
+            if ((da0 -= p0 * 2) > epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
+            else da0 = 0, a00 = a10 = (a0 + a1) / 2;
+            if ((da1 -= p1 * 2) > epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
+            else da1 = 0, a01 = a11 = (a0 + a1) / 2;
+          }
+    
+          var x01 = r1 * cos(a01),
+              y01 = r1 * sin(a01),
+              x10 = r0 * cos(a10),
+              y10 = r0 * sin(a10);
+    
+          // Apply rounded corners?
+          if (rc > epsilon) {
+            var x11 = r1 * cos(a11),
+                y11 = r1 * sin(a11),
+                x00 = r0 * cos(a00),
+                y00 = r0 * sin(a00),
+                oc;
+    
+            // Restrict the corner radius according to the sector angle.
+            if (da < pi && (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10))) {
+              var ax = x01 - oc[0],
+                  ay = y01 - oc[1],
+                  bx = x11 - oc[0],
+                  by = y11 - oc[1],
+                  kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2),
+                  lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
+              rc0 = min(rc, (r0 - lc) / (kc - 1));
+              rc1 = min(rc, (r1 - lc) / (kc + 1));
+            }
+          }
+    
+          // Is the sector collapsed to a line?
+          if (!(da1 > epsilon)) context.moveTo(x01, y01);
+    
+          // Does the sector’s outer ring have rounded corners?
+          else if (rc1 > epsilon) {
+            t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
+            t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
+    
+            context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
+    
+            // Have the corners merged?
+            if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
+    
+            // Otherwise, draw the two corners and the ring.
+            else {
+              context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
+              context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
+              context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
+            }
+          }
+    
+          // Or is the outer ring just a circular arc?
+          else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
+    
+          // Is there no inner ring, and it’s a circular sector?
+          // Or perhaps it’s an annular sector collapsed due to padding?
+          if (!(r0 > epsilon) || !(da0 > epsilon)) context.lineTo(x10, y10);
+    
+          // Does the sector’s inner ring (or point) have rounded corners?
+          else if (rc0 > epsilon) {
+            t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
+            t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
+    
+            context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
+    
+            // Have the corners merged?
+            if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
+    
+            // Otherwise, draw the two corners and the ring.
+            else {
+              context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
+              context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
+              context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
+            }
+          }
+    
+          // Or is the inner ring just a circular arc?
+          else context.arc(0, 0, r0, a10, a00, cw);
+        }
+    
+        context.closePath();
+    
+        if (buffer) return context = null, buffer + "" || null;
+      }
+    
+      arc.centroid = function() {
+        var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
+            a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2;
+        return [cos(a) * r, sin(a) * r];
+      };
+    
+      arc.innerRadius = function(_) {
+        return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant$1(+_), arc) : innerRadius;
+      };
+    
+      arc.outerRadius = function(_) {
+        return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant$1(+_), arc) : outerRadius;
+      };
+    
+      arc.cornerRadius = function(_) {
+        return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant$1(+_), arc) : cornerRadius;
+      };
+    
+      arc.padRadius = function(_) {
+        return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant$1(+_), arc) : padRadius;
+      };
+    
+      arc.startAngle = function(_) {
+        return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$1(+_), arc) : startAngle;
+      };
+    
+      arc.endAngle = function(_) {
+        return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$1(+_), arc) : endAngle;
+      };
+    
+      arc.padAngle = function(_) {
+        return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$1(+_), arc) : padAngle;
+      };
+    
+      arc.context = function(_) {
+        return arguments.length ? ((context = _ == null ? null : _), arc) : context;
+      };
+    
+      return arc;
+    }
+    
+    var slice = Array.prototype.slice;
+    
+    function array(x) {
+      return typeof x === "object" && "length" in x
+        ? x // Array, TypedArray, NodeList, array-like
+        : Array.from(x); // Map, Set, iterable, string, or anything else
+    }
+    
+    function Linear(context) {
+      this._context = context;
+    }
+    
+    Linear.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; // proceed
+          default: this._context.lineTo(x, y); break;
+        }
+      }
+    };
+    
+    function curveLinear(context) {
+      return new Linear(context);
+    }
+    
+    function x(p) {
+      return p[0];
+    }
+    
+    function y(p) {
+      return p[1];
+    }
+    
+    function line(x$1, y$1) {
+      var defined = constant$1(true),
+          context = null,
+          curve = curveLinear,
+          output = null;
+    
+      x$1 = typeof x$1 === "function" ? x$1 : (x$1 === undefined) ? x : constant$1(x$1);
+      y$1 = typeof y$1 === "function" ? y$1 : (y$1 === undefined) ? y : constant$1(y$1);
+    
+      function line(data) {
+        var i,
+            n = (data = array(data)).length,
+            d,
+            defined0 = false,
+            buffer;
+    
+        if (context == null) output = curve(buffer = path());
+    
+        for (i = 0; i <= n; ++i) {
+          if (!(i < n && defined(d = data[i], i, data)) === defined0) {
+            if (defined0 = !defined0) output.lineStart();
+            else output.lineEnd();
+          }
+          if (defined0) output.point(+x$1(d, i, data), +y$1(d, i, data));
+        }
+    
+        if (buffer) return output = null, buffer + "" || null;
+      }
+    
+      line.x = function(_) {
+        return arguments.length ? (x$1 = typeof _ === "function" ? _ : constant$1(+_), line) : x$1;
+      };
+    
+      line.y = function(_) {
+        return arguments.length ? (y$1 = typeof _ === "function" ? _ : constant$1(+_), line) : y$1;
+      };
+    
+      line.defined = function(_) {
+        return arguments.length ? (defined = typeof _ === "function" ? _ : constant$1(!!_), line) : defined;
+      };
+    
+      line.curve = function(_) {
+        return arguments.length ? (curve = _, context != null && (output = curve(context)), line) : curve;
+      };
+    
+      line.context = function(_) {
+        return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), line) : context;
+      };
+    
+      return line;
+    }
+    
+    function area(x0, y0, y1) {
+      var x1 = null,
+          defined = constant$1(true),
+          context = null,
+          curve = curveLinear,
+          output = null;
+    
+      x0 = typeof x0 === "function" ? x0 : (x0 === undefined) ? x : constant$1(+x0);
+      y0 = typeof y0 === "function" ? y0 : (y0 === undefined) ? constant$1(0) : constant$1(+y0);
+      y1 = typeof y1 === "function" ? y1 : (y1 === undefined) ? y : constant$1(+y1);
+    
+      function area(data) {
+        var i,
+            j,
+            k,
+            n = (data = array(data)).length,
+            d,
+            defined0 = false,
+            buffer,
+            x0z = new Array(n),
+            y0z = new Array(n);
+    
+        if (context == null) output = curve(buffer = path());
+    
+        for (i = 0; i <= n; ++i) {
+          if (!(i < n && defined(d = data[i], i, data)) === defined0) {
+            if (defined0 = !defined0) {
+              j = i;
+              output.areaStart();
+              output.lineStart();
+            } else {
+              output.lineEnd();
+              output.lineStart();
+              for (k = i - 1; k >= j; --k) {
+                output.point(x0z[k], y0z[k]);
+              }
+              output.lineEnd();
+              output.areaEnd();
+            }
+          }
+          if (defined0) {
+            x0z[i] = +x0(d, i, data), y0z[i] = +y0(d, i, data);
+            output.point(x1 ? +x1(d, i, data) : x0z[i], y1 ? +y1(d, i, data) : y0z[i]);
+          }
+        }
+    
+        if (buffer) return output = null, buffer + "" || null;
+      }
+    
+      function arealine() {
+        return line().defined(defined).curve(curve).context(context);
+      }
+    
+      area.x = function(_) {
+        return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$1(+_), x1 = null, area) : x0;
+      };
+    
+      area.x0 = function(_) {
+        return arguments.length ? (x0 = typeof _ === "function" ? _ : constant$1(+_), area) : x0;
+      };
+    
+      area.x1 = function(_) {
+        return arguments.length ? (x1 = _ == null ? null : typeof _ === "function" ? _ : constant$1(+_), area) : x1;
+      };
+    
+      area.y = function(_) {
+        return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$1(+_), y1 = null, area) : y0;
+      };
+    
+      area.y0 = function(_) {
+        return arguments.length ? (y0 = typeof _ === "function" ? _ : constant$1(+_), area) : y0;
+      };
+    
+      area.y1 = function(_) {
+        return arguments.length ? (y1 = _ == null ? null : typeof _ === "function" ? _ : constant$1(+_), area) : y1;
+      };
+    
+      area.lineX0 =
+      area.lineY0 = function() {
+        return arealine().x(x0).y(y0);
+      };
+    
+      area.lineY1 = function() {
+        return arealine().x(x0).y(y1);
+      };
+    
+      area.lineX1 = function() {
+        return arealine().x(x1).y(y0);
+      };
+    
+      area.defined = function(_) {
+        return arguments.length ? (defined = typeof _ === "function" ? _ : constant$1(!!_), area) : defined;
+      };
+    
+      area.curve = function(_) {
+        return arguments.length ? (curve = _, context != null && (output = curve(context)), area) : curve;
+      };
+    
+      area.context = function(_) {
+        return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), area) : context;
+      };
+    
+      return area;
+    }
+    
+    function descending$1(a, b) {
+      return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+    }
+    
+    function identity$1(d) {
+      return d;
+    }
+    
+    function pie() {
+      var value = identity$1,
+          sortValues = descending$1,
+          sort = null,
+          startAngle = constant$1(0),
+          endAngle = constant$1(tau),
+          padAngle = constant$1(0);
+    
+      function pie(data) {
+        var i,
+            n = (data = array(data)).length,
+            j,
+            k,
+            sum = 0,
+            index = new Array(n),
+            arcs = new Array(n),
+            a0 = +startAngle.apply(this, arguments),
+            da = Math.min(tau, Math.max(-tau, endAngle.apply(this, arguments) - a0)),
+            a1,
+            p = Math.min(Math.abs(da) / n, padAngle.apply(this, arguments)),
+            pa = p * (da < 0 ? -1 : 1),
+            v;
+    
+        for (i = 0; i < n; ++i) {
+          if ((v = arcs[index[i] = i] = +value(data[i], i, data)) > 0) {
+            sum += v;
+          }
+        }
+    
+        // Optionally sort the arcs by previously-computed values or by data.
+        if (sortValues != null) index.sort(function(i, j) { return sortValues(arcs[i], arcs[j]); });
+        else if (sort != null) index.sort(function(i, j) { return sort(data[i], data[j]); });
+    
+        // Compute the arcs! They are stored in the original data's order.
+        for (i = 0, k = sum ? (da - n * pa) / sum : 0; i < n; ++i, a0 = a1) {
+          j = index[i], v = arcs[j], a1 = a0 + (v > 0 ? v * k : 0) + pa, arcs[j] = {
+            data: data[j],
+            index: i,
+            value: v,
+            startAngle: a0,
+            endAngle: a1,
+            padAngle: p
+          };
+        }
+    
+        return arcs;
+      }
+    
+      pie.value = function(_) {
+        return arguments.length ? (value = typeof _ === "function" ? _ : constant$1(+_), pie) : value;
+      };
+    
+      pie.sortValues = function(_) {
+        return arguments.length ? (sortValues = _, sort = null, pie) : sortValues;
+      };
+    
+      pie.sort = function(_) {
+        return arguments.length ? (sort = _, sortValues = null, pie) : sort;
+      };
+    
+      pie.startAngle = function(_) {
+        return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : startAngle;
+      };
+    
+      pie.endAngle = function(_) {
+        return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : endAngle;
+      };
+    
+      pie.padAngle = function(_) {
+        return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant$1(+_), pie) : padAngle;
+      };
+    
+      return pie;
+    }
+    
+    var curveRadialLinear = curveRadial$1(curveLinear);
+    
+    function Radial(curve) {
+      this._curve = curve;
+    }
+    
+    Radial.prototype = {
+      areaStart: function() {
+        this._curve.areaStart();
+      },
+      areaEnd: function() {
+        this._curve.areaEnd();
+      },
+      lineStart: function() {
+        this._curve.lineStart();
+      },
+      lineEnd: function() {
+        this._curve.lineEnd();
+      },
+      point: function(a, r) {
+        this._curve.point(r * Math.sin(a), r * -Math.cos(a));
+      }
+    };
+    
+    function curveRadial$1(curve) {
+    
+      function radial(context) {
+        return new Radial(curve(context));
+      }
+    
+      radial._curve = curve;
+    
+      return radial;
+    }
+    
+    function lineRadial(l) {
+      var c = l.curve;
+    
+      l.angle = l.x, delete l.x;
+      l.radius = l.y, delete l.y;
+    
+      l.curve = function(_) {
+        return arguments.length ? c(curveRadial$1(_)) : c()._curve;
+      };
+    
+      return l;
+    }
+    
+    function lineRadial$1() {
+      return lineRadial(line().curve(curveRadialLinear));
+    }
+    
+    function areaRadial() {
+      var a = area().curve(curveRadialLinear),
+          c = a.curve,
+          x0 = a.lineX0,
+          x1 = a.lineX1,
+          y0 = a.lineY0,
+          y1 = a.lineY1;
+    
+      a.angle = a.x, delete a.x;
+      a.startAngle = a.x0, delete a.x0;
+      a.endAngle = a.x1, delete a.x1;
+      a.radius = a.y, delete a.y;
+      a.innerRadius = a.y0, delete a.y0;
+      a.outerRadius = a.y1, delete a.y1;
+      a.lineStartAngle = function() { return lineRadial(x0()); }, delete a.lineX0;
+      a.lineEndAngle = function() { return lineRadial(x1()); }, delete a.lineX1;
+      a.lineInnerRadius = function() { return lineRadial(y0()); }, delete a.lineY0;
+      a.lineOuterRadius = function() { return lineRadial(y1()); }, delete a.lineY1;
+    
+      a.curve = function(_) {
+        return arguments.length ? c(curveRadial$1(_)) : c()._curve;
+      };
+    
+      return a;
+    }
+    
+    function pointRadial(x, y) {
+      return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
+    }
+    
+    function linkSource(d) {
+      return d.source;
+    }
+    
+    function linkTarget(d) {
+      return d.target;
+    }
+    
+    function link(curve) {
+      var source = linkSource,
+          target = linkTarget,
+          x$1 = x,
+          y$1 = y,
+          context = null;
+    
+      function link() {
+        var buffer, argv = slice.call(arguments), s = source.apply(this, argv), t = target.apply(this, argv);
+        if (!context) context = buffer = path();
+        curve(context, +x$1.apply(this, (argv[0] = s, argv)), +y$1.apply(this, argv), +x$1.apply(this, (argv[0] = t, argv)), +y$1.apply(this, argv));
+        if (buffer) return context = null, buffer + "" || null;
+      }
+    
+      link.source = function(_) {
+        return arguments.length ? (source = _, link) : source;
+      };
+    
+      link.target = function(_) {
+        return arguments.length ? (target = _, link) : target;
+      };
+    
+      link.x = function(_) {
+        return arguments.length ? (x$1 = typeof _ === "function" ? _ : constant$1(+_), link) : x$1;
+      };
+    
+      link.y = function(_) {
+        return arguments.length ? (y$1 = typeof _ === "function" ? _ : constant$1(+_), link) : y$1;
+      };
+    
+      link.context = function(_) {
+        return arguments.length ? ((context = _ == null ? null : _), link) : context;
+      };
+    
+      return link;
+    }
+    
+    function curveHorizontal(context, x0, y0, x1, y1) {
+      context.moveTo(x0, y0);
+      context.bezierCurveTo(x0 = (x0 + x1) / 2, y0, x0, y1, x1, y1);
+    }
+    
+    function curveVertical(context, x0, y0, x1, y1) {
+      context.moveTo(x0, y0);
+      context.bezierCurveTo(x0, y0 = (y0 + y1) / 2, x1, y0, x1, y1);
+    }
+    
+    function curveRadial(context, x0, y0, x1, y1) {
+      var p0 = pointRadial(x0, y0),
+          p1 = pointRadial(x0, y0 = (y0 + y1) / 2),
+          p2 = pointRadial(x1, y0),
+          p3 = pointRadial(x1, y1);
+      context.moveTo(p0[0], p0[1]);
+      context.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
+    }
+    
+    function linkHorizontal() {
+      return link(curveHorizontal);
+    }
+    
+    function linkVertical() {
+      return link(curveVertical);
+    }
+    
+    function linkRadial() {
+      var l = link(curveRadial);
+      l.angle = l.x, delete l.x;
+      l.radius = l.y, delete l.y;
+      return l;
+    }
+    
+    var circle = {
+      draw: function(context, size) {
+        var r = Math.sqrt(size / pi);
+        context.moveTo(r, 0);
+        context.arc(0, 0, r, 0, tau);
+      }
+    };
+    
+    var cross = {
+      draw: function(context, size) {
+        var r = Math.sqrt(size / 5) / 2;
+        context.moveTo(-3 * r, -r);
+        context.lineTo(-r, -r);
+        context.lineTo(-r, -3 * r);
+        context.lineTo(r, -3 * r);
+        context.lineTo(r, -r);
+        context.lineTo(3 * r, -r);
+        context.lineTo(3 * r, r);
+        context.lineTo(r, r);
+        context.lineTo(r, 3 * r);
+        context.lineTo(-r, 3 * r);
+        context.lineTo(-r, r);
+        context.lineTo(-3 * r, r);
+        context.closePath();
+      }
+    };
+    
+    var tan30 = Math.sqrt(1 / 3),
+        tan30_2 = tan30 * 2;
+    
+    var diamond = {
+      draw: function(context, size) {
+        var y = Math.sqrt(size / tan30_2),
+            x = y * tan30;
+        context.moveTo(0, -y);
+        context.lineTo(x, 0);
+        context.lineTo(0, y);
+        context.lineTo(-x, 0);
+        context.closePath();
+      }
+    };
+    
+    var ka = 0.89081309152928522810,
+        kr = Math.sin(pi / 10) / Math.sin(7 * pi / 10),
+        kx = Math.sin(tau / 10) * kr,
+        ky = -Math.cos(tau / 10) * kr;
+    
+    var star = {
+      draw: function(context, size) {
+        var r = Math.sqrt(size * ka),
+            x = kx * r,
+            y = ky * r;
+        context.moveTo(0, -r);
+        context.lineTo(x, y);
+        for (var i = 1; i < 5; ++i) {
+          var a = tau * i / 5,
+              c = Math.cos(a),
+              s = Math.sin(a);
+          context.lineTo(s * r, -c * r);
+          context.lineTo(c * x - s * y, s * x + c * y);
+        }
+        context.closePath();
+      }
+    };
+    
+    var square = {
+      draw: function(context, size) {
+        var w = Math.sqrt(size),
+            x = -w / 2;
+        context.rect(x, x, w, w);
+      }
+    };
+    
+    var sqrt3 = Math.sqrt(3);
+    
+    var triangle = {
+      draw: function(context, size) {
+        var y = -Math.sqrt(size / (sqrt3 * 3));
+        context.moveTo(0, y * 2);
+        context.lineTo(-sqrt3 * y, -y);
+        context.lineTo(sqrt3 * y, -y);
+        context.closePath();
+      }
+    };
+    
+    var c = -0.5,
+        s = Math.sqrt(3) / 2,
+        k = 1 / Math.sqrt(12),
+        a = (k / 2 + 1) * 3;
+    
+    var wye = {
+      draw: function(context, size) {
+        var r = Math.sqrt(size / a),
+            x0 = r / 2,
+            y0 = r * k,
+            x1 = x0,
+            y1 = r * k + r,
+            x2 = -x1,
+            y2 = y1;
+        context.moveTo(x0, y0);
+        context.lineTo(x1, y1);
+        context.lineTo(x2, y2);
+        context.lineTo(c * x0 - s * y0, s * x0 + c * y0);
+        context.lineTo(c * x1 - s * y1, s * x1 + c * y1);
+        context.lineTo(c * x2 - s * y2, s * x2 + c * y2);
+        context.lineTo(c * x0 + s * y0, c * y0 - s * x0);
+        context.lineTo(c * x1 + s * y1, c * y1 - s * x1);
+        context.lineTo(c * x2 + s * y2, c * y2 - s * x2);
+        context.closePath();
+      }
+    };
+    
+    var symbols = [
+      circle,
+      cross,
+      diamond,
+      square,
+      star,
+      triangle,
+      wye
+    ];
+    
+    function symbol(type, size) {
+      var context = null;
+      type = typeof type === "function" ? type : constant$1(type || circle);
+      size = typeof size === "function" ? size : constant$1(size === undefined ? 64 : +size);
+    
+      function symbol() {
+        var buffer;
+        if (!context) context = buffer = path();
+        type.apply(this, arguments).draw(context, +size.apply(this, arguments));
+        if (buffer) return context = null, buffer + "" || null;
+      }
+    
+      symbol.type = function(_) {
+        return arguments.length ? (type = typeof _ === "function" ? _ : constant$1(_), symbol) : type;
+      };
+    
+      symbol.size = function(_) {
+        return arguments.length ? (size = typeof _ === "function" ? _ : constant$1(+_), symbol) : size;
+      };
+    
+      symbol.context = function(_) {
+        return arguments.length ? (context = _ == null ? null : _, symbol) : context;
+      };
+    
+      return symbol;
+    }
+    
+    function noop() {}
+    
+    function point$3(that, x, y) {
+      that._context.bezierCurveTo(
+        (2 * that._x0 + that._x1) / 3,
+        (2 * that._y0 + that._y1) / 3,
+        (that._x0 + 2 * that._x1) / 3,
+        (that._y0 + 2 * that._y1) / 3,
+        (that._x0 + 4 * that._x1 + x) / 6,
+        (that._y0 + 4 * that._y1 + y) / 6
+      );
+    }
+    
+    function Basis(context) {
+      this._context = context;
+    }
+    
+    Basis.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 =
+        this._y0 = this._y1 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 3: point$3(this, this._x1, this._y1); // proceed
+          case 2: this._context.lineTo(this._x1, this._y1); break;
+        }
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6); // proceed
+          default: point$3(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = x;
+        this._y0 = this._y1, this._y1 = y;
+      }
+    };
+    
+    function basis(context) {
+      return new Basis(context);
+    }
+    
+    function BasisClosed(context) {
+      this._context = context;
+    }
+    
+    BasisClosed.prototype = {
+      areaStart: noop,
+      areaEnd: noop,
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 = this._x3 = this._x4 =
+        this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 1: {
+            this._context.moveTo(this._x2, this._y2);
+            this._context.closePath();
+            break;
+          }
+          case 2: {
+            this._context.moveTo((this._x2 + 2 * this._x3) / 3, (this._y2 + 2 * this._y3) / 3);
+            this._context.lineTo((this._x3 + 2 * this._x2) / 3, (this._y3 + 2 * this._y2) / 3);
+            this._context.closePath();
+            break;
+          }
+          case 3: {
+            this.point(this._x2, this._y2);
+            this.point(this._x3, this._y3);
+            this.point(this._x4, this._y4);
+            break;
+          }
+        }
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._x2 = x, this._y2 = y; break;
+          case 1: this._point = 2; this._x3 = x, this._y3 = y; break;
+          case 2: this._point = 3; this._x4 = x, this._y4 = y; this._context.moveTo((this._x0 + 4 * this._x1 + x) / 6, (this._y0 + 4 * this._y1 + y) / 6); break;
+          default: point$3(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = x;
+        this._y0 = this._y1, this._y1 = y;
+      }
+    };
+    
+    function basisClosed(context) {
+      return new BasisClosed(context);
+    }
+    
+    function BasisOpen(context) {
+      this._context = context;
+    }
+    
+    BasisOpen.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 =
+        this._y0 = this._y1 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; var x0 = (this._x0 + 4 * this._x1 + x) / 6, y0 = (this._y0 + 4 * this._y1 + y) / 6; this._line ? this._context.lineTo(x0, y0) : this._context.moveTo(x0, y0); break;
+          case 3: this._point = 4; // proceed
+          default: point$3(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = x;
+        this._y0 = this._y1, this._y1 = y;
+      }
+    };
+    
+    function basisOpen(context) {
+      return new BasisOpen(context);
+    }
+    
+    class Bump {
+      constructor(context, x) {
+        this._context = context;
+        this._x = x;
+      }
+      areaStart() {
+        this._line = 0;
+      }
+      areaEnd() {
+        this._line = NaN;
+      }
+      lineStart() {
+        this._point = 0;
+      }
+      lineEnd() {
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      }
+      point(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: {
+            this._point = 1;
+            if (this._line) this._context.lineTo(x, y);
+            else this._context.moveTo(x, y);
+            break;
+          }
+          case 1: this._point = 2; // proceed
+          default: {
+            if (this._x) this._context.bezierCurveTo(this._x0 = (this._x0 + x) / 2, this._y0, this._x0, y, x, y);
+            else this._context.bezierCurveTo(this._x0, this._y0 = (this._y0 + y) / 2, x, this._y0, x, y);
+            break;
+          }
+        }
+        this._x0 = x, this._y0 = y;
+      }
+    }
+    
+    function bumpX(context) {
+      return new Bump(context, true);
+    }
+    
+    function bumpY(context) {
+      return new Bump(context, false);
+    }
+    
+    function Bundle(context, beta) {
+      this._basis = new Basis(context);
+      this._beta = beta;
+    }
+    
+    Bundle.prototype = {
+      lineStart: function() {
+        this._x = [];
+        this._y = [];
+        this._basis.lineStart();
+      },
+      lineEnd: function() {
+        var x = this._x,
+            y = this._y,
+            j = x.length - 1;
+    
+        if (j > 0) {
+          var x0 = x[0],
+              y0 = y[0],
+              dx = x[j] - x0,
+              dy = y[j] - y0,
+              i = -1,
+              t;
+    
+          while (++i <= j) {
+            t = i / j;
+            this._basis.point(
+              this._beta * x[i] + (1 - this._beta) * (x0 + t * dx),
+              this._beta * y[i] + (1 - this._beta) * (y0 + t * dy)
+            );
+          }
+        }
+    
+        this._x = this._y = null;
+        this._basis.lineEnd();
+      },
+      point: function(x, y) {
+        this._x.push(+x);
+        this._y.push(+y);
+      }
+    };
+    
+    var bundle = (function custom(beta) {
+    
+      function bundle(context) {
+        return beta === 1 ? new Basis(context) : new Bundle(context, beta);
+      }
+    
+      bundle.beta = function(beta) {
+        return custom(+beta);
+      };
+    
+      return bundle;
+    })(0.85);
+    
+    function point$2(that, x, y) {
+      that._context.bezierCurveTo(
+        that._x1 + that._k * (that._x2 - that._x0),
+        that._y1 + that._k * (that._y2 - that._y0),
+        that._x2 + that._k * (that._x1 - x),
+        that._y2 + that._k * (that._y1 - y),
+        that._x2,
+        that._y2
+      );
+    }
+    
+    function Cardinal(context, tension) {
+      this._context = context;
+      this._k = (1 - tension) / 6;
+    }
+    
+    Cardinal.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 =
+        this._y0 = this._y1 = this._y2 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 2: this._context.lineTo(this._x2, this._y2); break;
+          case 3: point$2(this, this._x1, this._y1); break;
+        }
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; this._x1 = x, this._y1 = y; break;
+          case 2: this._point = 3; // proceed
+          default: point$2(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var cardinal = (function custom(tension) {
+    
+      function cardinal(context) {
+        return new Cardinal(context, tension);
+      }
+    
+      cardinal.tension = function(tension) {
+        return custom(+tension);
+      };
+    
+      return cardinal;
+    })(0);
+    
+    function CardinalClosed(context, tension) {
+      this._context = context;
+      this._k = (1 - tension) / 6;
+    }
+    
+    CardinalClosed.prototype = {
+      areaStart: noop,
+      areaEnd: noop,
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 =
+        this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 1: {
+            this._context.moveTo(this._x3, this._y3);
+            this._context.closePath();
+            break;
+          }
+          case 2: {
+            this._context.lineTo(this._x3, this._y3);
+            this._context.closePath();
+            break;
+          }
+          case 3: {
+            this.point(this._x3, this._y3);
+            this.point(this._x4, this._y4);
+            this.point(this._x5, this._y5);
+            break;
+          }
+        }
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._x3 = x, this._y3 = y; break;
+          case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break;
+          case 2: this._point = 3; this._x5 = x, this._y5 = y; break;
+          default: point$2(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var cardinalClosed = (function custom(tension) {
+    
+      function cardinal(context) {
+        return new CardinalClosed(context, tension);
+      }
+    
+      cardinal.tension = function(tension) {
+        return custom(+tension);
+      };
+    
+      return cardinal;
+    })(0);
+    
+    function CardinalOpen(context, tension) {
+      this._context = context;
+      this._k = (1 - tension) / 6;
+    }
+    
+    CardinalOpen.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 =
+        this._y0 = this._y1 = this._y2 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break;
+          case 3: this._point = 4; // proceed
+          default: point$2(this, x, y); break;
+        }
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var cardinalOpen = (function custom(tension) {
+    
+      function cardinal(context) {
+        return new CardinalOpen(context, tension);
+      }
+    
+      cardinal.tension = function(tension) {
+        return custom(+tension);
+      };
+    
+      return cardinal;
+    })(0);
+    
+    function point$1(that, x, y) {
+      var x1 = that._x1,
+          y1 = that._y1,
+          x2 = that._x2,
+          y2 = that._y2;
+    
+      if (that._l01_a > epsilon) {
+        var a = 2 * that._l01_2a + 3 * that._l01_a * that._l12_a + that._l12_2a,
+            n = 3 * that._l01_a * (that._l01_a + that._l12_a);
+        x1 = (x1 * a - that._x0 * that._l12_2a + that._x2 * that._l01_2a) / n;
+        y1 = (y1 * a - that._y0 * that._l12_2a + that._y2 * that._l01_2a) / n;
+      }
+    
+      if (that._l23_a > epsilon) {
+        var b = 2 * that._l23_2a + 3 * that._l23_a * that._l12_a + that._l12_2a,
+            m = 3 * that._l23_a * (that._l23_a + that._l12_a);
+        x2 = (x2 * b + that._x1 * that._l23_2a - x * that._l12_2a) / m;
+        y2 = (y2 * b + that._y1 * that._l23_2a - y * that._l12_2a) / m;
+      }
+    
+      that._context.bezierCurveTo(x1, y1, x2, y2, that._x2, that._y2);
+    }
+    
+    function CatmullRom(context, alpha) {
+      this._context = context;
+      this._alpha = alpha;
+    }
+    
+    CatmullRom.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 =
+        this._y0 = this._y1 = this._y2 = NaN;
+        this._l01_a = this._l12_a = this._l23_a =
+        this._l01_2a = this._l12_2a = this._l23_2a =
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 2: this._context.lineTo(this._x2, this._y2); break;
+          case 3: this.point(this._x2, this._y2); break;
+        }
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+    
+        if (this._point) {
+          var x23 = this._x2 - x,
+              y23 = this._y2 - y;
+          this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+        }
+    
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; // proceed
+          default: point$1(this, x, y); break;
+        }
+    
+        this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+        this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var catmullRom = (function custom(alpha) {
+    
+      function catmullRom(context) {
+        return alpha ? new CatmullRom(context, alpha) : new Cardinal(context, 0);
+      }
+    
+      catmullRom.alpha = function(alpha) {
+        return custom(+alpha);
+      };
+    
+      return catmullRom;
+    })(0.5);
+    
+    function CatmullRomClosed(context, alpha) {
+      this._context = context;
+      this._alpha = alpha;
+    }
+    
+    CatmullRomClosed.prototype = {
+      areaStart: noop,
+      areaEnd: noop,
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._x5 =
+        this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = this._y5 = NaN;
+        this._l01_a = this._l12_a = this._l23_a =
+        this._l01_2a = this._l12_2a = this._l23_2a =
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 1: {
+            this._context.moveTo(this._x3, this._y3);
+            this._context.closePath();
+            break;
+          }
+          case 2: {
+            this._context.lineTo(this._x3, this._y3);
+            this._context.closePath();
+            break;
+          }
+          case 3: {
+            this.point(this._x3, this._y3);
+            this.point(this._x4, this._y4);
+            this.point(this._x5, this._y5);
+            break;
+          }
+        }
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+    
+        if (this._point) {
+          var x23 = this._x2 - x,
+              y23 = this._y2 - y;
+          this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+        }
+    
+        switch (this._point) {
+          case 0: this._point = 1; this._x3 = x, this._y3 = y; break;
+          case 1: this._point = 2; this._context.moveTo(this._x4 = x, this._y4 = y); break;
+          case 2: this._point = 3; this._x5 = x, this._y5 = y; break;
+          default: point$1(this, x, y); break;
+        }
+    
+        this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+        this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var catmullRomClosed = (function custom(alpha) {
+    
+      function catmullRom(context) {
+        return alpha ? new CatmullRomClosed(context, alpha) : new CardinalClosed(context, 0);
+      }
+    
+      catmullRom.alpha = function(alpha) {
+        return custom(+alpha);
+      };
+    
+      return catmullRom;
+    })(0.5);
+    
+    function CatmullRomOpen(context, alpha) {
+      this._context = context;
+      this._alpha = alpha;
+    }
+    
+    CatmullRomOpen.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 = this._x2 =
+        this._y0 = this._y1 = this._y2 = NaN;
+        this._l01_a = this._l12_a = this._l23_a =
+        this._l01_2a = this._l12_2a = this._l23_2a =
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._line || (this._line !== 0 && this._point === 3)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+    
+        if (this._point) {
+          var x23 = this._x2 - x,
+              y23 = this._y2 - y;
+          this._l23_a = Math.sqrt(this._l23_2a = Math.pow(x23 * x23 + y23 * y23, this._alpha));
+        }
+    
+        switch (this._point) {
+          case 0: this._point = 1; break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; this._line ? this._context.lineTo(this._x2, this._y2) : this._context.moveTo(this._x2, this._y2); break;
+          case 3: this._point = 4; // proceed
+          default: point$1(this, x, y); break;
+        }
+    
+        this._l01_a = this._l12_a, this._l12_a = this._l23_a;
+        this._l01_2a = this._l12_2a, this._l12_2a = this._l23_2a;
+        this._x0 = this._x1, this._x1 = this._x2, this._x2 = x;
+        this._y0 = this._y1, this._y1 = this._y2, this._y2 = y;
+      }
+    };
+    
+    var catmullRomOpen = (function custom(alpha) {
+    
+      function catmullRom(context) {
+        return alpha ? new CatmullRomOpen(context, alpha) : new CardinalOpen(context, 0);
+      }
+    
+      catmullRom.alpha = function(alpha) {
+        return custom(+alpha);
+      };
+    
+      return catmullRom;
+    })(0.5);
+    
+    function LinearClosed(context) {
+      this._context = context;
+    }
+    
+    LinearClosed.prototype = {
+      areaStart: noop,
+      areaEnd: noop,
+      lineStart: function() {
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (this._point) this._context.closePath();
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        if (this._point) this._context.lineTo(x, y);
+        else this._point = 1, this._context.moveTo(x, y);
+      }
+    };
+    
+    function linearClosed(context) {
+      return new LinearClosed(context);
+    }
+    
+    function sign(x) {
+      return x < 0 ? -1 : 1;
+    }
+    
+    // Calculate the slopes of the tangents (Hermite-type interpolation) based on
+    // the following paper: Steffen, M. 1990. A Simple Method for Monotonic
+    // Interpolation in One Dimension. Astronomy and Astrophysics, Vol. 239, NO.
+    // NOV(II), P. 443, 1990.
+    function slope3(that, x2, y2) {
+      var h0 = that._x1 - that._x0,
+          h1 = x2 - that._x1,
+          s0 = (that._y1 - that._y0) / (h0 || h1 < 0 && -0),
+          s1 = (y2 - that._y1) / (h1 || h0 < 0 && -0),
+          p = (s0 * h1 + s1 * h0) / (h0 + h1);
+      return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;
+    }
+    
+    // Calculate a one-sided slope.
+    function slope2(that, t) {
+      var h = that._x1 - that._x0;
+      return h ? (3 * (that._y1 - that._y0) / h - t) / 2 : t;
+    }
+    
+    // According to https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Representations
+    // "you can express cubic Hermite interpolation in terms of cubic Bézier curves
+    // with respect to the four values p0, p0 + m0 / 3, p1 - m1 / 3, p1".
+    function point(that, t0, t1) {
+      var x0 = that._x0,
+          y0 = that._y0,
+          x1 = that._x1,
+          y1 = that._y1,
+          dx = (x1 - x0) / 3;
+      that._context.bezierCurveTo(x0 + dx, y0 + dx * t0, x1 - dx, y1 - dx * t1, x1, y1);
+    }
+    
+    function MonotoneX(context) {
+      this._context = context;
+    }
+    
+    MonotoneX.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x0 = this._x1 =
+        this._y0 = this._y1 =
+        this._t0 = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        switch (this._point) {
+          case 2: this._context.lineTo(this._x1, this._y1); break;
+          case 3: point(this, this._t0, slope2(this, this._t0)); break;
+        }
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        var t1 = NaN;
+    
+        x = +x, y = +y;
+        if (x === this._x1 && y === this._y1) return; // Ignore coincident points.
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; break;
+          case 2: this._point = 3; point(this, slope2(this, t1 = slope3(this, x, y)), t1); break;
+          default: point(this, this._t0, t1 = slope3(this, x, y)); break;
+        }
+    
+        this._x0 = this._x1, this._x1 = x;
+        this._y0 = this._y1, this._y1 = y;
+        this._t0 = t1;
+      }
+    };
+    
+    function MonotoneY(context) {
+      this._context = new ReflectContext(context);
+    }
+    
+    (MonotoneY.prototype = Object.create(MonotoneX.prototype)).point = function(x, y) {
+      MonotoneX.prototype.point.call(this, y, x);
+    };
+    
+    function ReflectContext(context) {
+      this._context = context;
+    }
+    
+    ReflectContext.prototype = {
+      moveTo: function(x, y) { this._context.moveTo(y, x); },
+      closePath: function() { this._context.closePath(); },
+      lineTo: function(x, y) { this._context.lineTo(y, x); },
+      bezierCurveTo: function(x1, y1, x2, y2, x, y) { this._context.bezierCurveTo(y1, x1, y2, x2, y, x); }
+    };
+    
+    function monotoneX(context) {
+      return new MonotoneX(context);
+    }
+    
+    function monotoneY(context) {
+      return new MonotoneY(context);
+    }
+    
+    function Natural(context) {
+      this._context = context;
+    }
+    
+    Natural.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x = [];
+        this._y = [];
+      },
+      lineEnd: function() {
+        var x = this._x,
+            y = this._y,
+            n = x.length;
+    
+        if (n) {
+          this._line ? this._context.lineTo(x[0], y[0]) : this._context.moveTo(x[0], y[0]);
+          if (n === 2) {
+            this._context.lineTo(x[1], y[1]);
+          } else {
+            var px = controlPoints(x),
+                py = controlPoints(y);
+            for (var i0 = 0, i1 = 1; i1 < n; ++i0, ++i1) {
+              this._context.bezierCurveTo(px[0][i0], py[0][i0], px[1][i0], py[1][i0], x[i1], y[i1]);
+            }
+          }
+        }
+    
+        if (this._line || (this._line !== 0 && n === 1)) this._context.closePath();
+        this._line = 1 - this._line;
+        this._x = this._y = null;
+      },
+      point: function(x, y) {
+        this._x.push(+x);
+        this._y.push(+y);
+      }
+    };
+    
+    // See https://www.particleincell.com/2012/bezier-splines/ for derivation.
+    function controlPoints(x) {
+      var i,
+          n = x.length - 1,
+          m,
+          a = new Array(n),
+          b = new Array(n),
+          r = new Array(n);
+      a[0] = 0, b[0] = 2, r[0] = x[0] + 2 * x[1];
+      for (i = 1; i < n - 1; ++i) a[i] = 1, b[i] = 4, r[i] = 4 * x[i] + 2 * x[i + 1];
+      a[n - 1] = 2, b[n - 1] = 7, r[n - 1] = 8 * x[n - 1] + x[n];
+      for (i = 1; i < n; ++i) m = a[i] / b[i - 1], b[i] -= m, r[i] -= m * r[i - 1];
+      a[n - 1] = r[n - 1] / b[n - 1];
+      for (i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i];
+      b[n - 1] = (x[n] + a[n - 1]) / 2;
+      for (i = 0; i < n - 1; ++i) b[i] = 2 * x[i + 1] - a[i + 1];
+      return [a, b];
+    }
+    
+    function natural(context) {
+      return new Natural(context);
+    }
+    
+    function Step(context, t) {
+      this._context = context;
+      this._t = t;
+    }
+    
+    Step.prototype = {
+      areaStart: function() {
+        this._line = 0;
+      },
+      areaEnd: function() {
+        this._line = NaN;
+      },
+      lineStart: function() {
+        this._x = this._y = NaN;
+        this._point = 0;
+      },
+      lineEnd: function() {
+        if (0 < this._t && this._t < 1 && this._point === 2) this._context.lineTo(this._x, this._y);
+        if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath();
+        if (this._line >= 0) this._t = 1 - this._t, this._line = 1 - this._line;
+      },
+      point: function(x, y) {
+        x = +x, y = +y;
+        switch (this._point) {
+          case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break;
+          case 1: this._point = 2; // proceed
+          default: {
+            if (this._t <= 0) {
+              this._context.lineTo(this._x, y);
+              this._context.lineTo(x, y);
+            } else {
+              var x1 = this._x * (1 - this._t) + x * this._t;
+              this._context.lineTo(x1, this._y);
+              this._context.lineTo(x1, y);
+            }
+            break;
+          }
+        }
+        this._x = x, this._y = y;
+      }
+    };
+    
+    function step(context) {
+      return new Step(context, 0.5);
+    }
+    
+    function stepBefore(context) {
+      return new Step(context, 0);
+    }
+    
+    function stepAfter(context) {
+      return new Step(context, 1);
+    }
+    
+    function none$1(series, order) {
+      if (!((n = series.length) > 1)) return;
+      for (var i = 1, j, s0, s1 = series[order[0]], n, m = s1.length; i < n; ++i) {
+        s0 = s1, s1 = series[order[i]];
+        for (j = 0; j < m; ++j) {
+          s1[j][1] += s1[j][0] = isNaN(s0[j][1]) ? s0[j][0] : s0[j][1];
+        }
+      }
+    }
+    
+    function none(series) {
+      var n = series.length, o = new Array(n);
+      while (--n >= 0) o[n] = n;
+      return o;
+    }
+    
+    function stackValue(d, key) {
+      return d[key];
+    }
+    
+    function stackSeries(key) {
+      const series = [];
+      series.key = key;
+      return series;
+    }
+    
+    function stack() {
+      var keys = constant$1([]),
+          order = none,
+          offset = none$1,
+          value = stackValue;
+    
+      function stack(data) {
+        var sz = Array.from(keys.apply(this, arguments), stackSeries),
+            i, n = sz.length, j = -1,
+            oz;
+    
+        for (const d of data) {
+          for (i = 0, ++j; i < n; ++i) {
+            (sz[i][j] = [0, +value(d, sz[i].key, j, data)]).data = d;
+          }
+        }
+    
+        for (i = 0, oz = array(order(sz)); i < n; ++i) {
+          sz[oz[i]].index = i;
+        }
+    
+        offset(sz, oz);
+        return sz;
+      }
+    
+      stack.keys = function(_) {
+        return arguments.length ? (keys = typeof _ === "function" ? _ : constant$1(Array.from(_)), stack) : keys;
+      };
+    
+      stack.value = function(_) {
+        return arguments.length ? (value = typeof _ === "function" ? _ : constant$1(+_), stack) : value;
+      };
+    
+      stack.order = function(_) {
+        return arguments.length ? (order = _ == null ? none : typeof _ === "function" ? _ : constant$1(Array.from(_)), stack) : order;
+      };
+    
+      stack.offset = function(_) {
+        return arguments.length ? (offset = _ == null ? none$1 : _, stack) : offset;
+      };
+    
+      return stack;
+    }
+    
+    function expand(series, order) {
+      if (!((n = series.length) > 0)) return;
+      for (var i, n, j = 0, m = series[0].length, y; j < m; ++j) {
+        for (y = i = 0; i < n; ++i) y += series[i][j][1] || 0;
+        if (y) for (i = 0; i < n; ++i) series[i][j][1] /= y;
+      }
+      none$1(series, order);
+    }
+    
+    function diverging(series, order) {
+      if (!((n = series.length) > 0)) return;
+      for (var i, j = 0, d, dy, yp, yn, n, m = series[order[0]].length; j < m; ++j) {
+        for (yp = yn = 0, i = 0; i < n; ++i) {
+          if ((dy = (d = series[order[i]][j])[1] - d[0]) > 0) {
+            d[0] = yp, d[1] = yp += dy;
+          } else if (dy < 0) {
+            d[1] = yn, d[0] = yn += dy;
+          } else {
+            d[0] = 0, d[1] = dy;
+          }
+        }
+      }
+    }
+    
+    function silhouette(series, order) {
+      if (!((n = series.length) > 0)) return;
+      for (var j = 0, s0 = series[order[0]], n, m = s0.length; j < m; ++j) {
+        for (var i = 0, y = 0; i < n; ++i) y += series[i][j][1] || 0;
+        s0[j][1] += s0[j][0] = -y / 2;
+      }
+      none$1(series, order);
+    }
+    
+    function wiggle(series, order) {
+      if (!((n = series.length) > 0) || !((m = (s0 = series[order[0]]).length) > 0)) return;
+      for (var y = 0, j = 1, s0, m, n; j < m; ++j) {
+        for (var i = 0, s1 = 0, s2 = 0; i < n; ++i) {
+          var si = series[order[i]],
+              sij0 = si[j][1] || 0,
+              sij1 = si[j - 1][1] || 0,
+              s3 = (sij0 - sij1) / 2;
+          for (var k = 0; k < i; ++k) {
+            var sk = series[order[k]],
+                skj0 = sk[j][1] || 0,
+                skj1 = sk[j - 1][1] || 0;
+            s3 += skj0 - skj1;
+          }
+          s1 += sij0, s2 += s3 * sij0;
+        }
+        s0[j - 1][1] += s0[j - 1][0] = y;
+        if (s1) y -= s2 / s1;
+      }
+      s0[j - 1][1] += s0[j - 1][0] = y;
+      none$1(series, order);
+    }
+    
+    function appearance(series) {
+      var peaks = series.map(peak);
+      return none(series).sort(function(a, b) { return peaks[a] - peaks[b]; });
+    }
+    
+    function peak(series) {
+      var i = -1, j = 0, n = series.length, vi, vj = -Infinity;
+      while (++i < n) if ((vi = +series[i][1]) > vj) vj = vi, j = i;
+      return j;
+    }
+    
+    function ascending(series) {
+      var sums = series.map(sum);
+      return none(series).sort(function(a, b) { return sums[a] - sums[b]; });
+    }
+    
+    function sum(series) {
+      var s = 0, i = -1, n = series.length, v;
+      while (++i < n) if (v = +series[i][1]) s += v;
+      return s;
+    }
+    
+    function descending(series) {
+      return ascending(series).reverse();
+    }
+    
+    function insideOut(series) {
+      var n = series.length,
+          i,
+          j,
+          sums = series.map(sum),
+          order = appearance(series),
+          top = 0,
+          bottom = 0,
+          tops = [],
+          bottoms = [];
+    
+      for (i = 0; i < n; ++i) {
+        j = order[i];
+        if (top < bottom) {
+          top += sums[j];
+          tops.push(j);
+        } else {
+          bottom += sums[j];
+          bottoms.push(j);
+        }
+      }
+    
+      return bottoms.reverse().concat(tops);
+    }
+    
+    function reverse(series) {
+      return none(series).reverse();
+    }
+    
+    var constant = x => () => x;
+    
+    function ZoomEvent(type, {
+      sourceEvent,
+      target,
+      transform,
+      dispatch
+    }) {
+      Object.defineProperties(this, {
+        type: {value: type, enumerable: true, configurable: true},
+        sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},
+        target: {value: target, enumerable: true, configurable: true},
+        transform: {value: transform, enumerable: true, configurable: true},
+        _: {value: dispatch}
+      });
+    }
+    
+    function Transform(k, x, y) {
+      this.k = k;
+      this.x = x;
+      this.y = y;
+    }
+    
+    Transform.prototype = {
+      constructor: Transform,
+      scale: function(k) {
+        return k === 1 ? this : new Transform(this.k * k, this.x, this.y);
+      },
+      translate: function(x, y) {
+        return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);
+      },
+      apply: function(point) {
+        return [point[0] * this.k + this.x, point[1] * this.k + this.y];
+      },
+      applyX: function(x) {
+        return x * this.k + this.x;
+      },
+      applyY: function(y) {
+        return y * this.k + this.y;
+      },
+      invert: function(location) {
+        return [(location[0] - this.x) / this.k, (location[1] - this.y) / this.k];
+      },
+      invertX: function(x) {
+        return (x - this.x) / this.k;
+      },
+      invertY: function(y) {
+        return (y - this.y) / this.k;
+      },
+      rescaleX: function(x) {
+        return x.copy().domain(x.range().map(this.invertX, this).map(x.invert, x));
+      },
+      rescaleY: function(y) {
+        return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y));
+      },
+      toString: function() {
+        return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
+      }
+    };
+    
+    var identity = new Transform(1, 0, 0);
+    
+    transform.prototype = Transform.prototype;
+    
+    function transform(node) {
+      while (!node.__zoom) if (!(node = node.parentNode)) return identity;
+      return node.__zoom;
+    }
+    
+    function nopropagation(event) {
+      event.stopImmediatePropagation();
+    }
+    
+    function noevent(event) {
+      event.preventDefault();
+      event.stopImmediatePropagation();
+    }
+    
+    // Ignore right-click, since that should open the context menu.
+    // except for pinch-to-zoom, which is sent as a wheel+ctrlKey event
+    function defaultFilter(event) {
+      return (!event.ctrlKey || event.type === 'wheel') && !event.button;
+    }
+    
+    function defaultExtent() {
+      var e = this;
+      if (e instanceof SVGElement) {
+        e = e.ownerSVGElement || e;
+        if (e.hasAttribute("viewBox")) {
+          e = e.viewBox.baseVal;
+          return [[e.x, e.y], [e.x + e.width, e.y + e.height]];
+        }
+        return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];
+      }
+      return [[0, 0], [e.clientWidth, e.clientHeight]];
+    }
+    
+    function defaultTransform() {
+      return this.__zoom || identity;
+    }
+    
+    function defaultWheelDelta(event) {
+      return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * (event.ctrlKey ? 10 : 1);
+    }
+    
+    function defaultTouchable() {
+      return navigator.maxTouchPoints || ("ontouchstart" in this);
+    }
+    
+    function defaultConstrain(transform, extent, translateExtent) {
+      var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],
+          dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],
+          dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],
+          dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];
+      return transform.translate(
+        dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
+        dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
+      );
+    }
+    
+    function zoom() {
+      var filter = defaultFilter,
+          extent = defaultExtent,
+          constrain = defaultConstrain,
+          wheelDelta = defaultWheelDelta,
+          touchable = defaultTouchable,
+          scaleExtent = [0, Infinity],
+          translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],
+          duration = 250,
+          interpolate = interpolateZoom,
+          listeners = dispatch("start", "zoom", "end"),
+          touchstarting,
+          touchfirst,
+          touchending,
+          touchDelay = 500,
+          wheelDelay = 150,
+          clickDistance2 = 0,
+          tapDistance = 10;
+    
+      function zoom(selection) {
+        selection
+            .property("__zoom", defaultTransform)
+            .on("wheel.zoom", wheeled)
+            .on("mousedown.zoom", mousedowned)
+            .on("dblclick.zoom", dblclicked)
+          .filter(touchable)
+            .on("touchstart.zoom", touchstarted)
+            .on("touchmove.zoom", touchmoved)
+            .on("touchend.zoom touchcancel.zoom", touchended)
+            .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+      }
+    
+      zoom.transform = function(collection, transform, point, event) {
+        var selection = collection.selection ? collection.selection() : collection;
+        selection.property("__zoom", defaultTransform);
+        if (collection !== selection) {
+          schedule(collection, transform, point, event);
+        } else {
+          selection.interrupt().each(function() {
+            gesture(this, arguments)
+              .event(event)
+              .start()
+              .zoom(null, typeof transform === "function" ? transform.apply(this, arguments) : transform)
+              .end();
+          });
+        }
+      };
+    
+      zoom.scaleBy = function(selection, k, p, event) {
+        zoom.scaleTo(selection, function() {
+          var k0 = this.__zoom.k,
+              k1 = typeof k === "function" ? k.apply(this, arguments) : k;
+          return k0 * k1;
+        }, p, event);
+      };
+    
+      zoom.scaleTo = function(selection, k, p, event) {
+        zoom.transform(selection, function() {
+          var e = extent.apply(this, arguments),
+              t0 = this.__zoom,
+              p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p,
+              p1 = t0.invert(p0),
+              k1 = typeof k === "function" ? k.apply(this, arguments) : k;
+          return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
+        }, p, event);
+      };
+    
+      zoom.translateBy = function(selection, x, y, event) {
+        zoom.transform(selection, function() {
+          return constrain(this.__zoom.translate(
+            typeof x === "function" ? x.apply(this, arguments) : x,
+            typeof y === "function" ? y.apply(this, arguments) : y
+          ), extent.apply(this, arguments), translateExtent);
+        }, null, event);
+      };
+    
+      zoom.translateTo = function(selection, x, y, p, event) {
+        zoom.transform(selection, function() {
+          var e = extent.apply(this, arguments),
+              t = this.__zoom,
+              p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
+          return constrain(identity.translate(p0[0], p0[1]).scale(t.k).translate(
+            typeof x === "function" ? -x.apply(this, arguments) : -x,
+            typeof y === "function" ? -y.apply(this, arguments) : -y
+          ), e, translateExtent);
+        }, p, event);
+      };
+    
+      function scale(transform, k) {
+        k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));
+        return k === transform.k ? transform : new Transform(k, transform.x, transform.y);
+      }
+    
+      function translate(transform, p0, p1) {
+        var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;
+        return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);
+      }
+    
+      function centroid(extent) {
+        return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];
+      }
+    
+      function schedule(transition, transform, point, event) {
+        transition
+            .on("start.zoom", function() { gesture(this, arguments).event(event).start(); })
+            .on("interrupt.zoom end.zoom", function() { gesture(this, arguments).event(event).end(); })
+            .tween("zoom", function() {
+              var that = this,
+                  args = arguments,
+                  g = gesture(that, args).event(event),
+                  e = extent.apply(that, args),
+                  p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point,
+                  w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),
+                  a = that.__zoom,
+                  b = typeof transform === "function" ? transform.apply(that, args) : transform,
+                  i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
+              return function(t) {
+                if (t === 1) t = b; // Avoid rounding error on end.
+                else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }
+                g.zoom(null, t);
+              };
+            });
+      }
+    
+      function gesture(that, args, clean) {
+        return (!clean && that.__zooming) || new Gesture(that, args);
+      }
+    
+      function Gesture(that, args) {
+        this.that = that;
+        this.args = args;
+        this.active = 0;
+        this.sourceEvent = null;
+        this.extent = extent.apply(that, args);
+        this.taps = 0;
+      }
+    
+      Gesture.prototype = {
+        event: function(event) {
+          if (event) this.sourceEvent = event;
+          return this;
+        },
+        start: function() {
+          if (++this.active === 1) {
+            this.that.__zooming = this;
+            this.emit("start");
+          }
+          return this;
+        },
+        zoom: function(key, transform) {
+          if (this.mouse && key !== "mouse") this.mouse[1] = transform.invert(this.mouse[0]);
+          if (this.touch0 && key !== "touch") this.touch0[1] = transform.invert(this.touch0[0]);
+          if (this.touch1 && key !== "touch") this.touch1[1] = transform.invert(this.touch1[0]);
+          this.that.__zoom = transform;
+          this.emit("zoom");
+          return this;
+        },
+        end: function() {
+          if (--this.active === 0) {
+            delete this.that.__zooming;
+            this.emit("end");
+          }
+          return this;
+        },
+        emit: function(type) {
+          var d = select(this.that).datum();
+          listeners.call(
+            type,
+            this.that,
+            new ZoomEvent(type, {
+              sourceEvent: this.sourceEvent,
+              target: zoom,
+              type,
+              transform: this.that.__zoom,
+              dispatch: listeners
+            }),
+            d
+          );
+        }
+      };
+    
+      function wheeled(event, ...args) {
+        if (!filter.apply(this, arguments)) return;
+        var g = gesture(this, args).event(event),
+            t = this.__zoom,
+            k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),
+            p = pointer(event);
+    
+        // If the mouse is in the same location as before, reuse it.
+        // If there were recent wheel events, reset the wheel idle timeout.
+        if (g.wheel) {
+          if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {
+            g.mouse[1] = t.invert(g.mouse[0] = p);
+          }
+          clearTimeout(g.wheel);
+        }
+    
+        // If this wheel event won’t trigger a transform change, ignore it.
+        else if (t.k === k) return;
+    
+        // Otherwise, capture the mouse point and location at the start.
+        else {
+          g.mouse = [p, t.invert(p)];
+          interrupt(this);
+          g.start();
+        }
+    
+        noevent(event);
+        g.wheel = setTimeout(wheelidled, wheelDelay);
+        g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));
+    
+        function wheelidled() {
+          g.wheel = null;
+          g.end();
+        }
+      }
+    
+      function mousedowned(event, ...args) {
+        if (touchending || !filter.apply(this, arguments)) return;
+        var g = gesture(this, args, true).event(event),
+            v = select(event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true),
+            p = pointer(event, currentTarget),
+            currentTarget = event.currentTarget,
+            x0 = event.clientX,
+            y0 = event.clientY;
+    
+        dragDisable(event.view);
+        nopropagation(event);
+        g.mouse = [p, this.__zoom.invert(p)];
+        interrupt(this);
+        g.start();
+    
+        function mousemoved(event) {
+          noevent(event);
+          if (!g.moved) {
+            var dx = event.clientX - x0, dy = event.clientY - y0;
+            g.moved = dx * dx + dy * dy > clickDistance2;
+          }
+          g.event(event)
+           .zoom("mouse", constrain(translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]), g.extent, translateExtent));
+        }
+    
+        function mouseupped(event) {
+          v.on("mousemove.zoom mouseup.zoom", null);
+          yesdrag(event.view, g.moved);
+          noevent(event);
+          g.event(event).end();
+        }
+      }
+    
+      function dblclicked(event, ...args) {
+        if (!filter.apply(this, arguments)) return;
+        var t0 = this.__zoom,
+            p0 = pointer(event.changedTouches ? event.changedTouches[0] : event, this),
+            p1 = t0.invert(p0),
+            k1 = t0.k * (event.shiftKey ? 0.5 : 2),
+            t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent);
+    
+        noevent(event);
+        if (duration > 0) select(this).transition().duration(duration).call(schedule, t1, p0, event);
+        else select(this).call(zoom.transform, t1, p0, event);
+      }
+    
+      function touchstarted(event, ...args) {
+        if (!filter.apply(this, arguments)) return;
+        var touches = event.touches,
+            n = touches.length,
+            g = gesture(this, args, event.changedTouches.length === n).event(event),
+            started, i, t, p;
+    
+        nopropagation(event);
+        for (i = 0; i < n; ++i) {
+          t = touches[i], p = pointer(t, this);
+          p = [p, this.__zoom.invert(p), t.identifier];
+          if (!g.touch0) g.touch0 = p, started = true, g.taps = 1 + !!touchstarting;
+          else if (!g.touch1 && g.touch0[2] !== p[2]) g.touch1 = p, g.taps = 0;
+        }
+    
+        if (touchstarting) touchstarting = clearTimeout(touchstarting);
+    
+        if (started) {
+          if (g.taps < 2) touchfirst = p[0], touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay);
+          interrupt(this);
+          g.start();
+        }
+      }
+    
+      function touchmoved(event, ...args) {
+        if (!this.__zooming) return;
+        var g = gesture(this, args).event(event),
+            touches = event.changedTouches,
+            n = touches.length, i, t, p, l;
+    
+        noevent(event);
+        for (i = 0; i < n; ++i) {
+          t = touches[i], p = pointer(t, this);
+          if (g.touch0 && g.touch0[2] === t.identifier) g.touch0[0] = p;
+          else if (g.touch1 && g.touch1[2] === t.identifier) g.touch1[0] = p;
+        }
+        t = g.that.__zoom;
+        if (g.touch1) {
+          var p0 = g.touch0[0], l0 = g.touch0[1],
+              p1 = g.touch1[0], l1 = g.touch1[1],
+              dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,
+              dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;
+          t = scale(t, Math.sqrt(dp / dl));
+          p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
+          l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];
+        }
+        else if (g.touch0) p = g.touch0[0], l = g.touch0[1];
+        else return;
+    
+        g.zoom("touch", constrain(translate(t, p, l), g.extent, translateExtent));
+      }
+    
+      function touchended(event, ...args) {
+        if (!this.__zooming) return;
+        var g = gesture(this, args).event(event),
+            touches = event.changedTouches,
+            n = touches.length, i, t;
+    
+        nopropagation(event);
+        if (touchending) clearTimeout(touchending);
+        touchending = setTimeout(function() { touchending = null; }, touchDelay);
+        for (i = 0; i < n; ++i) {
+          t = touches[i];
+          if (g.touch0 && g.touch0[2] === t.identifier) delete g.touch0;
+          else if (g.touch1 && g.touch1[2] === t.identifier) delete g.touch1;
+        }
+        if (g.touch1 && !g.touch0) g.touch0 = g.touch1, delete g.touch1;
+        if (g.touch0) g.touch0[1] = this.__zoom.invert(g.touch0[0]);
+        else {
+          g.end();
+          // If this was a dbltap, reroute to the (optional) dblclick.zoom handler.
+          if (g.taps === 2) {
+            t = pointer(t, this);
+            if (Math.hypot(touchfirst[0] - t[0], touchfirst[1] - t[1]) < tapDistance) {
+              var p = select(this).on("dblclick.zoom");
+              if (p) p.apply(this, arguments);
+            }
+          }
+        }
+      }
+    
+      zoom.wheelDelta = function(_) {
+        return arguments.length ? (wheelDelta = typeof _ === "function" ? _ : constant(+_), zoom) : wheelDelta;
+      };
+    
+      zoom.filter = function(_) {
+        return arguments.length ? (filter = typeof _ === "function" ? _ : constant(!!_), zoom) : filter;
+      };
+    
+      zoom.touchable = function(_) {
+        return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), zoom) : touchable;
+      };
+    
+      zoom.extent = function(_) {
+        return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
+      };
+    
+      zoom.scaleExtent = function(_) {
+        return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
+      };
+    
+      zoom.translateExtent = function(_) {
+        return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];
+      };
+    
+      zoom.constrain = function(_) {
+        return arguments.length ? (constrain = _, zoom) : constrain;
+      };
+    
+      zoom.duration = function(_) {
+        return arguments.length ? (duration = +_, zoom) : duration;
+      };
+    
+      zoom.interpolate = function(_) {
+        return arguments.length ? (interpolate = _, zoom) : interpolate;
+      };
+    
+      zoom.on = function() {
+        var value = listeners.on.apply(listeners, arguments);
+        return value === listeners ? zoom : value;
+      };
+    
+      zoom.clickDistance = function(_) {
+        return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
+      };
+    
+      zoom.tapDistance = function(_) {
+        return arguments.length ? (tapDistance = +_, zoom) : tapDistance;
+      };
+    
+      return zoom;
+    }
+    
+    exports.Adder = Adder;
+    exports.Delaunay = Delaunay;
+    exports.FormatSpecifier = FormatSpecifier;
+    exports.InternMap = InternMap;
+    exports.InternSet = InternSet;
+    exports.Voronoi = Voronoi;
+    exports.active = active;
+    exports.arc = arc;
+    exports.area = area;
+    exports.areaRadial = areaRadial;
+    exports.ascending = ascending$3;
+    exports.autoType = autoType;
+    exports.axisBottom = axisBottom;
+    exports.axisLeft = axisLeft;
+    exports.axisRight = axisRight;
+    exports.axisTop = axisTop;
+    exports.bin = bin;
+    exports.bisect = bisectRight;
+    exports.bisectCenter = bisectCenter;
+    exports.bisectLeft = bisectLeft;
+    exports.bisectRight = bisectRight;
+    exports.bisector = bisector;
+    exports.blob = blob;
+    exports.brush = brush;
+    exports.brushSelection = brushSelection;
+    exports.brushX = brushX;
+    exports.brushY = brushY;
+    exports.buffer = buffer;
+    exports.chord = chord;
+    exports.chordDirected = chordDirected;
+    exports.chordTranspose = chordTranspose;
+    exports.cluster = cluster;
+    exports.color = color;
+    exports.contourDensity = density;
+    exports.contours = contours;
+    exports.count = count$1;
+    exports.create = create$1;
+    exports.creator = creator;
+    exports.cross = cross$2;
+    exports.csv = csv;
+    exports.csvFormat = csvFormat;
+    exports.csvFormatBody = csvFormatBody;
+    exports.csvFormatRow = csvFormatRow;
+    exports.csvFormatRows = csvFormatRows;
+    exports.csvFormatValue = csvFormatValue;
+    exports.csvParse = csvParse;
+    exports.csvParseRows = csvParseRows;
+    exports.cubehelix = cubehelix$3;
+    exports.cumsum = cumsum;
+    exports.curveBasis = basis;
+    exports.curveBasisClosed = basisClosed;
+    exports.curveBasisOpen = basisOpen;
+    exports.curveBumpX = bumpX;
+    exports.curveBumpY = bumpY;
+    exports.curveBundle = bundle;
+    exports.curveCardinal = cardinal;
+    exports.curveCardinalClosed = cardinalClosed;
+    exports.curveCardinalOpen = cardinalOpen;
+    exports.curveCatmullRom = catmullRom;
+    exports.curveCatmullRomClosed = catmullRomClosed;
+    exports.curveCatmullRomOpen = catmullRomOpen;
+    exports.curveLinear = curveLinear;
+    exports.curveLinearClosed = linearClosed;
+    exports.curveMonotoneX = monotoneX;
+    exports.curveMonotoneY = monotoneY;
+    exports.curveNatural = natural;
+    exports.curveStep = step;
+    exports.curveStepAfter = stepAfter;
+    exports.curveStepBefore = stepBefore;
+    exports.descending = descending$2;
+    exports.deviation = deviation;
+    exports.difference = difference;
+    exports.disjoint = disjoint;
+    exports.dispatch = dispatch;
+    exports.drag = drag;
+    exports.dragDisable = dragDisable;
+    exports.dragEnable = yesdrag;
+    exports.dsv = dsv;
+    exports.dsvFormat = dsvFormat;
+    exports.easeBack = backInOut;
+    exports.easeBackIn = backIn;
+    exports.easeBackInOut = backInOut;
+    exports.easeBackOut = backOut;
+    exports.easeBounce = bounceOut;
+    exports.easeBounceIn = bounceIn;
+    exports.easeBounceInOut = bounceInOut;
+    exports.easeBounceOut = bounceOut;
+    exports.easeCircle = circleInOut;
+    exports.easeCircleIn = circleIn;
+    exports.easeCircleInOut = circleInOut;
+    exports.easeCircleOut = circleOut;
+    exports.easeCubic = cubicInOut;
+    exports.easeCubicIn = cubicIn;
+    exports.easeCubicInOut = cubicInOut;
+    exports.easeCubicOut = cubicOut;
+    exports.easeElastic = elasticOut;
+    exports.easeElasticIn = elasticIn;
+    exports.easeElasticInOut = elasticInOut;
+    exports.easeElasticOut = elasticOut;
+    exports.easeExp = expInOut;
+    exports.easeExpIn = expIn;
+    exports.easeExpInOut = expInOut;
+    exports.easeExpOut = expOut;
+    exports.easeLinear = linear$1;
+    exports.easePoly = polyInOut;
+    exports.easePolyIn = polyIn;
+    exports.easePolyInOut = polyInOut;
+    exports.easePolyOut = polyOut;
+    exports.easeQuad = quadInOut;
+    exports.easeQuadIn = quadIn;
+    exports.easeQuadInOut = quadInOut;
+    exports.easeQuadOut = quadOut;
+    exports.easeSin = sinInOut;
+    exports.easeSinIn = sinIn;
+    exports.easeSinInOut = sinInOut;
+    exports.easeSinOut = sinOut;
+    exports.every = every;
+    exports.extent = extent$1;
+    exports.fcumsum = fcumsum;
+    exports.filter = filter$1;
+    exports.forceCenter = center;
+    exports.forceCollide = collide;
+    exports.forceLink = link$2;
+    exports.forceManyBody = manyBody;
+    exports.forceRadial = radial$1;
+    exports.forceSimulation = simulation;
+    exports.forceX = x$1;
+    exports.forceY = y$1;
+    exports.formatDefaultLocale = defaultLocale$1;
+    exports.formatLocale = formatLocale$1;
+    exports.formatSpecifier = formatSpecifier;
+    exports.fsum = fsum;
+    exports.geoAlbers = albers;
+    exports.geoAlbersUsa = albersUsa;
+    exports.geoArea = area$2;
+    exports.geoAzimuthalEqualArea = azimuthalEqualArea;
+    exports.geoAzimuthalEqualAreaRaw = azimuthalEqualAreaRaw;
+    exports.geoAzimuthalEquidistant = azimuthalEquidistant;
+    exports.geoAzimuthalEquidistantRaw = azimuthalEquidistantRaw;
+    exports.geoBounds = bounds;
+    exports.geoCentroid = centroid$1;
+    exports.geoCircle = circle$2;
+    exports.geoClipAntimeridian = clipAntimeridian;
+    exports.geoClipCircle = clipCircle;
+    exports.geoClipExtent = extent;
+    exports.geoClipRectangle = clipRectangle;
+    exports.geoConicConformal = conicConformal;
+    exports.geoConicConformalRaw = conicConformalRaw;
+    exports.geoConicEqualArea = conicEqualArea;
+    exports.geoConicEqualAreaRaw = conicEqualAreaRaw;
+    exports.geoConicEquidistant = conicEquidistant;
+    exports.geoConicEquidistantRaw = conicEquidistantRaw;
+    exports.geoContains = contains$1;
+    exports.geoDistance = distance;
+    exports.geoEqualEarth = equalEarth;
+    exports.geoEqualEarthRaw = equalEarthRaw;
+    exports.geoEquirectangular = equirectangular;
+    exports.geoEquirectangularRaw = equirectangularRaw;
+    exports.geoGnomonic = gnomonic;
+    exports.geoGnomonicRaw = gnomonicRaw;
+    exports.geoGraticule = graticule;
+    exports.geoGraticule10 = graticule10;
+    exports.geoIdentity = identity$4;
+    exports.geoInterpolate = interpolate;
+    exports.geoLength = length$1;
+    exports.geoMercator = mercator;
+    exports.geoMercatorRaw = mercatorRaw;
+    exports.geoNaturalEarth1 = naturalEarth1;
+    exports.geoNaturalEarth1Raw = naturalEarth1Raw;
+    exports.geoOrthographic = orthographic;
+    exports.geoOrthographicRaw = orthographicRaw;
+    exports.geoPath = index$2;
+    exports.geoProjection = projection;
+    exports.geoProjectionMutator = projectionMutator;
+    exports.geoRotation = rotation;
+    exports.geoStereographic = stereographic;
+    exports.geoStereographicRaw = stereographicRaw;
+    exports.geoStream = geoStream;
+    exports.geoTransform = transform$1;
+    exports.geoTransverseMercator = transverseMercator;
+    exports.geoTransverseMercatorRaw = transverseMercatorRaw;
+    exports.gray = gray;
+    exports.greatest = greatest;
+    exports.greatestIndex = greatestIndex;
+    exports.group = group;
+    exports.groupSort = groupSort;
+    exports.groups = groups;
+    exports.hcl = hcl$2;
+    exports.hierarchy = hierarchy;
+    exports.histogram = bin;
+    exports.hsl = hsl$2;
+    exports.html = html;
+    exports.image = image;
+    exports.index = index$4;
+    exports.indexes = indexes;
+    exports.interpolate = interpolate$2;
+    exports.interpolateArray = array$3;
+    exports.interpolateBasis = basis$2;
+    exports.interpolateBasisClosed = basisClosed$1;
+    exports.interpolateBlues = Blues;
+    exports.interpolateBrBG = BrBG;
+    exports.interpolateBuGn = BuGn;
+    exports.interpolateBuPu = BuPu;
+    exports.interpolateCividis = cividis;
+    exports.interpolateCool = cool;
+    exports.interpolateCubehelix = cubehelix$2;
+    exports.interpolateCubehelixDefault = cubehelix;
+    exports.interpolateCubehelixLong = cubehelixLong;
+    exports.interpolateDate = date$1;
+    exports.interpolateDiscrete = discrete;
+    exports.interpolateGnBu = GnBu;
+    exports.interpolateGreens = Greens;
+    exports.interpolateGreys = Greys;
+    exports.interpolateHcl = hcl$1;
+    exports.interpolateHclLong = hclLong;
+    exports.interpolateHsl = hsl$1;
+    exports.interpolateHslLong = hslLong;
+    exports.interpolateHue = hue;
+    exports.interpolateInferno = inferno;
+    exports.interpolateLab = lab;
+    exports.interpolateMagma = magma;
+    exports.interpolateNumber = interpolateNumber;
+    exports.interpolateNumberArray = numberArray;
+    exports.interpolateObject = object$1;
+    exports.interpolateOrRd = OrRd;
+    exports.interpolateOranges = Oranges;
+    exports.interpolatePRGn = PRGn;
+    exports.interpolatePiYG = PiYG;
+    exports.interpolatePlasma = plasma;
+    exports.interpolatePuBu = PuBu;
+    exports.interpolatePuBuGn = PuBuGn;
+    exports.interpolatePuOr = PuOr;
+    exports.interpolatePuRd = PuRd;
+    exports.interpolatePurples = Purples;
+    exports.interpolateRainbow = rainbow;
+    exports.interpolateRdBu = RdBu;
+    exports.interpolateRdGy = RdGy;
+    exports.interpolateRdPu = RdPu;
+    exports.interpolateRdYlBu = RdYlBu;
+    exports.interpolateRdYlGn = RdYlGn;
+    exports.interpolateReds = Reds;
+    exports.interpolateRgb = interpolateRgb;
+    exports.interpolateRgbBasis = rgbBasis;
+    exports.interpolateRgbBasisClosed = rgbBasisClosed;
+    exports.interpolateRound = interpolateRound;
+    exports.interpolateSinebow = sinebow;
+    exports.interpolateSpectral = Spectral;
+    exports.interpolateString = interpolateString;
+    exports.interpolateTransformCss = interpolateTransformCss;
+    exports.interpolateTransformSvg = interpolateTransformSvg;
+    exports.interpolateTurbo = turbo;
+    exports.interpolateViridis = viridis;
+    exports.interpolateWarm = warm;
+    exports.interpolateYlGn = YlGn;
+    exports.interpolateYlGnBu = YlGnBu;
+    exports.interpolateYlOrBr = YlOrBr;
+    exports.interpolateYlOrRd = YlOrRd;
+    exports.interpolateZoom = interpolateZoom;
+    exports.interrupt = interrupt;
+    exports.intersection = intersection;
+    exports.interval = interval;
+    exports.isoFormat = formatIso;
+    exports.isoParse = parseIso;
+    exports.json = json;
+    exports.lab = lab$1;
+    exports.lch = lch;
+    exports.least = least;
+    exports.leastIndex = leastIndex;
+    exports.line = line;
+    exports.lineRadial = lineRadial$1;
+    exports.linkHorizontal = linkHorizontal;
+    exports.linkRadial = linkRadial;
+    exports.linkVertical = linkVertical;
+    exports.local = local$1;
+    exports.map = map$1;
+    exports.matcher = matcher;
+    exports.max = max$3;
+    exports.maxIndex = maxIndex;
+    exports.mean = mean;
+    exports.median = median;
+    exports.merge = merge;
+    exports.min = min$2;
+    exports.minIndex = minIndex;
+    exports.namespace = namespace;
+    exports.namespaces = namespaces;
+    exports.nice = nice$1;
+    exports.now = now;
+    exports.pack = index$1;
+    exports.packEnclose = enclose;
+    exports.packSiblings = siblings;
+    exports.pairs = pairs;
+    exports.partition = partition;
+    exports.path = path;
+    exports.permute = permute;
+    exports.pie = pie;
+    exports.piecewise = piecewise;
+    exports.pointRadial = pointRadial;
+    exports.pointer = pointer;
+    exports.pointers = pointers;
+    exports.polygonArea = area$1;
+    exports.polygonCentroid = centroid;
+    exports.polygonContains = contains;
+    exports.polygonHull = hull;
+    exports.polygonLength = length;
+    exports.precisionFixed = precisionFixed;
+    exports.precisionPrefix = precisionPrefix;
+    exports.precisionRound = precisionRound;
+    exports.quadtree = quadtree;
+    exports.quantile = quantile$1;
+    exports.quantileSorted = quantileSorted;
+    exports.quantize = quantize$1;
+    exports.quickselect = quickselect;
+    exports.radialArea = areaRadial;
+    exports.radialLine = lineRadial$1;
+    exports.randomBates = bates;
+    exports.randomBernoulli = bernoulli;
+    exports.randomBeta = beta;
+    exports.randomBinomial = binomial;
+    exports.randomCauchy = cauchy;
+    exports.randomExponential = exponential;
+    exports.randomGamma = gamma;
+    exports.randomGeometric = geometric;
+    exports.randomInt = int;
+    exports.randomIrwinHall = irwinHall;
+    exports.randomLcg = lcg;
+    exports.randomLogNormal = logNormal;
+    exports.randomLogistic = logistic;
+    exports.randomNormal = normal;
+    exports.randomPareto = pareto;
+    exports.randomPoisson = poisson;
+    exports.randomUniform = uniform;
+    exports.randomWeibull = weibull;
+    exports.range = sequence;
+    exports.reduce = reduce;
+    exports.reverse = reverse$1;
+    exports.rgb = rgb;
+    exports.ribbon = ribbon$1;
+    exports.ribbonArrow = ribbonArrow;
+    exports.rollup = rollup;
+    exports.rollups = rollups;
+    exports.scaleBand = band;
+    exports.scaleDiverging = diverging$1;
+    exports.scaleDivergingLog = divergingLog;
+    exports.scaleDivergingPow = divergingPow;
+    exports.scaleDivergingSqrt = divergingSqrt;
+    exports.scaleDivergingSymlog = divergingSymlog;
+    exports.scaleIdentity = identity$2;
+    exports.scaleImplicit = implicit;
+    exports.scaleLinear = linear;
+    exports.scaleLog = log;
+    exports.scaleOrdinal = ordinal;
+    exports.scalePoint = point$4;
+    exports.scalePow = pow;
+    exports.scaleQuantile = quantile;
+    exports.scaleQuantize = quantize;
+    exports.scaleRadial = radial;
+    exports.scaleSequential = sequential;
+    exports.scaleSequentialLog = sequentialLog;
+    exports.scaleSequentialPow = sequentialPow;
+    exports.scaleSequentialQuantile = sequentialQuantile;
+    exports.scaleSequentialSqrt = sequentialSqrt;
+    exports.scaleSequentialSymlog = sequentialSymlog;
+    exports.scaleSqrt = sqrt$1;
+    exports.scaleSymlog = symlog;
+    exports.scaleThreshold = threshold;
+    exports.scaleTime = time;
+    exports.scaleUtc = utcTime;
+    exports.scan = scan;
+    exports.schemeAccent = Accent;
+    exports.schemeBlues = scheme$5;
+    exports.schemeBrBG = scheme$q;
+    exports.schemeBuGn = scheme$h;
+    exports.schemeBuPu = scheme$g;
+    exports.schemeCategory10 = category10;
+    exports.schemeDark2 = Dark2;
+    exports.schemeGnBu = scheme$f;
+    exports.schemeGreens = scheme$4;
+    exports.schemeGreys = scheme$3;
+    exports.schemeOrRd = scheme$e;
+    exports.schemeOranges = scheme;
+    exports.schemePRGn = scheme$p;
+    exports.schemePaired = Paired;
+    exports.schemePastel1 = Pastel1;
+    exports.schemePastel2 = Pastel2;
+    exports.schemePiYG = scheme$o;
+    exports.schemePuBu = scheme$c;
+    exports.schemePuBuGn = scheme$d;
+    exports.schemePuOr = scheme$n;
+    exports.schemePuRd = scheme$b;
+    exports.schemePurples = scheme$2;
+    exports.schemeRdBu = scheme$m;
+    exports.schemeRdGy = scheme$l;
+    exports.schemeRdPu = scheme$a;
+    exports.schemeRdYlBu = scheme$k;
+    exports.schemeRdYlGn = scheme$j;
+    exports.schemeReds = scheme$1;
+    exports.schemeSet1 = Set1;
+    exports.schemeSet2 = Set2;
+    exports.schemeSet3 = Set3;
+    exports.schemeSpectral = scheme$i;
+    exports.schemeTableau10 = Tableau10;
+    exports.schemeYlGn = scheme$8;
+    exports.schemeYlGnBu = scheme$9;
+    exports.schemeYlOrBr = scheme$7;
+    exports.schemeYlOrRd = scheme$6;
+    exports.select = select;
+    exports.selectAll = selectAll;
+    exports.selection = selection;
+    exports.selector = selector;
+    exports.selectorAll = selectorAll;
+    exports.shuffle = shuffle$1;
+    exports.shuffler = shuffler;
+    exports.some = some;
+    exports.sort = sort;
+    exports.stack = stack;
+    exports.stackOffsetDiverging = diverging;
+    exports.stackOffsetExpand = expand;
+    exports.stackOffsetNone = none$1;
+    exports.stackOffsetSilhouette = silhouette;
+    exports.stackOffsetWiggle = wiggle;
+    exports.stackOrderAppearance = appearance;
+    exports.stackOrderAscending = ascending;
+    exports.stackOrderDescending = descending;
+    exports.stackOrderInsideOut = insideOut;
+    exports.stackOrderNone = none;
+    exports.stackOrderReverse = reverse;
+    exports.stratify = stratify;
+    exports.style = styleValue;
+    exports.subset = subset;
+    exports.sum = sum$1;
+    exports.superset = superset;
+    exports.svg = svg;
+    exports.symbol = symbol;
+    exports.symbolCircle = circle;
+    exports.symbolCross = cross;
+    exports.symbolDiamond = diamond;
+    exports.symbolSquare = square;
+    exports.symbolStar = star;
+    exports.symbolTriangle = triangle;
+    exports.symbolWye = wye;
+    exports.symbols = symbols;
+    exports.text = text;
+    exports.thresholdFreedmanDiaconis = freedmanDiaconis;
+    exports.thresholdScott = scott;
+    exports.thresholdSturges = thresholdSturges;
+    exports.tickFormat = tickFormat;
+    exports.tickIncrement = tickIncrement;
+    exports.tickStep = tickStep;
+    exports.ticks = ticks;
+    exports.timeDay = day;
+    exports.timeDays = days;
+    exports.timeFormatDefaultLocale = defaultLocale;
+    exports.timeFormatLocale = formatLocale;
+    exports.timeFriday = friday;
+    exports.timeFridays = fridays;
+    exports.timeHour = hour;
+    exports.timeHours = hours;
+    exports.timeInterval = newInterval;
+    exports.timeMillisecond = millisecond;
+    exports.timeMilliseconds = milliseconds;
+    exports.timeMinute = minute;
+    exports.timeMinutes = minutes;
+    exports.timeMonday = monday;
+    exports.timeMondays = mondays;
+    exports.timeMonth = month;
+    exports.timeMonths = months;
+    exports.timeSaturday = saturday;
+    exports.timeSaturdays = saturdays;
+    exports.timeSecond = second;
+    exports.timeSeconds = seconds;
+    exports.timeSunday = sunday;
+    exports.timeSundays = sundays;
+    exports.timeThursday = thursday;
+    exports.timeThursdays = thursdays;
+    exports.timeTickInterval = timeTickInterval;
+    exports.timeTicks = timeTicks;
+    exports.timeTuesday = tuesday;
+    exports.timeTuesdays = tuesdays;
+    exports.timeWednesday = wednesday;
+    exports.timeWednesdays = wednesdays;
+    exports.timeWeek = sunday;
+    exports.timeWeeks = sundays;
+    exports.timeYear = year;
+    exports.timeYears = years;
+    exports.timeout = timeout;
+    exports.timer = timer;
+    exports.timerFlush = timerFlush;
+    exports.transition = transition;
+    exports.transpose = transpose;
+    exports.tree = tree;
+    exports.treemap = index;
+    exports.treemapBinary = binary;
+    exports.treemapDice = treemapDice;
+    exports.treemapResquarify = resquarify;
+    exports.treemapSlice = treemapSlice;
+    exports.treemapSliceDice = sliceDice;
+    exports.treemapSquarify = squarify;
+    exports.tsv = tsv;
+    exports.tsvFormat = tsvFormat;
+    exports.tsvFormatBody = tsvFormatBody;
+    exports.tsvFormatRow = tsvFormatRow;
+    exports.tsvFormatRows = tsvFormatRows;
+    exports.tsvFormatValue = tsvFormatValue;
+    exports.tsvParse = tsvParse;
+    exports.tsvParseRows = tsvParseRows;
+    exports.union = union;
+    exports.utcDay = utcDay;
+    exports.utcDays = utcDays;
+    exports.utcFriday = utcFriday;
+    exports.utcFridays = utcFridays;
+    exports.utcHour = utcHour;
+    exports.utcHours = utcHours;
+    exports.utcMillisecond = millisecond;
+    exports.utcMilliseconds = milliseconds;
+    exports.utcMinute = utcMinute;
+    exports.utcMinutes = utcMinutes;
+    exports.utcMonday = utcMonday;
+    exports.utcMondays = utcMondays;
+    exports.utcMonth = utcMonth;
+    exports.utcMonths = utcMonths;
+    exports.utcSaturday = utcSaturday;
+    exports.utcSaturdays = utcSaturdays;
+    exports.utcSecond = second;
+    exports.utcSeconds = seconds;
+    exports.utcSunday = utcSunday;
+    exports.utcSundays = utcSundays;
+    exports.utcThursday = utcThursday;
+    exports.utcThursdays = utcThursdays;
+    exports.utcTickInterval = utcTickInterval;
+    exports.utcTicks = utcTicks;
+    exports.utcTuesday = utcTuesday;
+    exports.utcTuesdays = utcTuesdays;
+    exports.utcWednesday = utcWednesday;
+    exports.utcWednesdays = utcWednesdays;
+    exports.utcWeek = utcSunday;
+    exports.utcWeeks = utcSundays;
+    exports.utcYear = utcYear;
+    exports.utcYears = utcYears;
+    exports.variance = variance;
+    exports.version = version;
+    exports.window = defaultView;
+    exports.xml = xml;
+    exports.zip = zip;
+    exports.zoom = zoom;
+    exports.zoomIdentity = identity;
+    exports.zoomTransform = transform;
+    
+    Object.defineProperty(exports, '__esModule', { value: true });
+    
+    })));
+    
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/libs/jquery.min.js b/system/javascript/osapjs/client/libs/jquery.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d9b3a258759c53e7bc66b6fc554c51e2434437c
--- /dev/null
+++ b/system/javascript/osapjs/client/libs/jquery.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:"jQuery"+("3.3.1"+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==c.call(e))&&(!(t=i(e))||"function"==typeof(n=f.call(t,"constructor")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),"function"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function C(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",I="\\["+M+"*("+R+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+R+"))|)"+M+"*\\]",W=":("+R+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+I+")*)|.*)\\)|)",$=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),F=new RegExp("^"+M+"*,"+M+"*"),_=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=fe(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=pe(t);function ye(){}ye.prototype=r.filters=r.pseudos,r.setFilters=new ye,a=oe.tokenize=function(e,t){var n,i,o,a,s,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=r.preFilter;while(s){n&&!(i=F.exec(s))||(i&&(s=s.slice(i[0].length)||s),u.push(o=[])),n=!1,(i=_.exec(s))&&(n=i.shift(),o.push({value:n,type:i[0].replace(B," ")}),s=s.slice(n.length));for(a in r.filter)!(i=V[a].exec(s))||l[a]&&!(i=l[a](i))||(n=i.shift(),o.push({value:n,type:a,matches:i}),s=s.slice(n.length));if(!n)break}return t?s.length:s?oe.error(e):k(e,u).slice(0)};function ve(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function me(e,t,n){var r=t.dir,i=t.next,o=i||r,a=n&&"parentNode"===o,s=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||a)return e(t,n,i);return!1}:function(t,n,u){var l,c,f,p=[T,s];if(u){while(t=t[r])if((1===t.nodeType||a)&&e(t,n,u))return!0}else while(t=t[r])if(1===t.nodeType||a)if(f=t[b]||(t[b]={}),c=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((l=c[o])&&l[0]===T&&l[1]===s)return p[2]=l[2];if(c[o]=p,p[2]=e(t,n,u))return!0}return!1}}function xe(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r<i;r++)oe(e,t[r],n);return n}function we(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Te(e,t,n,r,i,o){return r&&!r[b]&&(r=Te(r)),i&&!i[b]&&(i=Te(i,o)),se(function(o,a,s,u){var l,c,f,p=[],d=[],h=a.length,g=o||be(t||"*",s.nodeType?[s]:s,[]),y=!e||!o&&t?g:we(g,p,e,s,u),v=n?i||(o?e:h||r)?[]:a:y;if(n&&n(y,v,s,u),r){l=we(v,d),r(l,[],s,u),c=l.length;while(c--)(f=l[c])&&(v[d[c]]=!(y[d[c]]=f))}if(o){if(i||e){if(i){l=[],c=v.length;while(c--)(f=v[c])&&l.push(y[c]=f);i(null,v=[],l,u)}c=v.length;while(c--)(f=v[c])&&(l=i?O(o,f):p[c])>-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u<o;u++)if(n=r.relative[e[u].type])p=[me(xe(p),n)];else{if((n=r.filter[e[u].type].apply(null,e[u].matches))[b]){for(i=++u;i<o;i++)if(r.relative[e[i].type])break;return Te(u>1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u<i&&Ce(e.slice(u,i)),i<o&&Ce(e=e.slice(i)),i<o&&ve(e))}p.push(n)}return xe(p)}function Ee(e,t){var n=t.length>0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t<r;t++)if(w.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)w.find(e,i[t],n);return r>1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(w.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&w(e);if(!D.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s<o.length)!1===o[s].apply(n[0],n[1])&&e.stopOnFalse&&(s=o.length,n=!1)}e.memory||(n=!1),t=!1,i&&(o=n?[]:"")},l={add:function(){return o&&(n&&!t&&(s=o.length-1,a.push(n)),function t(n){w.each(n,function(n,r){g(r)?e.unique&&l.has(r)||o.push(r):r&&r.length&&"string"!==x(r)&&t(r)})}(arguments),n&&!t&&u()),this},remove:function(){return w.each(arguments,function(e,t){var n;while((n=w.inArray(t,o,n))>-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t<o)){if((e=r.apply(s,u))===n.promise())throw new TypeError("Thenable self-resolution");l=e&&("object"==typeof e||"function"==typeof e)&&e.then,g(l)?i?l.call(e,a(o,n,I,i),a(o,n,W,i)):(o++,l.call(e,a(o,n,I,i),a(o,n,W,i),a(o,n,I,n.notifyWith))):(r!==I&&(s=void 0,u=[e]),(i||n.resolveWith)(s,u))}},c=i?l:function(){try{l()}catch(e){w.Deferred.exceptionHook&&w.Deferred.exceptionHook(e,c.stackTrace),t+1>=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},X=/^-ms-/,U=/-([a-z])/g;function V(e,t){return t.toUpperCase()}function G(e){return e.replace(X,"ms-").replace(U,V)}var Y=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Q(){this.expando=w.expando+Q.uid++}Q.uid=1,Q.prototype={cache:function(e){var t=e[this.expando];return t||(t={},Y(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[G(t)]=n;else for(r in t)i[G(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][G(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(G):(t=G(t))in r?[t]:t.match(M)||[]).length;while(n--)delete r[t[n]]}(void 0===t||w.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!w.isEmptyObject(t)}};var J=new Q,K=new Q,Z=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,ee=/[A-Z]/g;function te(e){return"true"===e||"false"!==e&&("null"===e?null:e===+e+""?+e:Z.test(e)?JSON.parse(e):e)}function ne(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(ee,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n=te(n)}catch(e){}K.set(e,t,n)}else n=void 0;return n}w.extend({hasData:function(e){return K.hasData(e)||J.hasData(e)},data:function(e,t,n){return K.access(e,t,n)},removeData:function(e,t){K.remove(e,t)},_data:function(e,t,n){return J.access(e,t,n)},_removeData:function(e,t){J.remove(e,t)}}),w.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=K.get(o),1===o.nodeType&&!J.get(o,"hasDataAttrs"))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf("data-")&&(r=G(r.slice(5)),ne(o,r,i[r]));J.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof e?this.each(function(){K.set(this,e)}):z(this,function(t){var n;if(o&&void 0===t){if(void 0!==(n=K.get(o,e)))return n;if(void 0!==(n=ne(o,e)))return n}else this.each(function(){K.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?w.queue(this[0],e):void 0===t?this:this.each(function(){var n=w.queue(this,e,t);w._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&w.dequeue(this,e)})},dequeue:function(e){return this.each(function(){w.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=w.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=J.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var re=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ie=new RegExp("^(?:([+-])=|)("+re+")([a-z%]*)$","i"),oe=["Top","Right","Bottom","Left"],ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&w.contains(e.ownerDocument,e)&&"none"===w.css(e,"display")},se=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};function ue(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return w.css(e,t,"")},u=s(),l=n&&n[3]||(w.cssNumber[t]?"":"px"),c=(w.cssNumber[t]||"px"!==l&&+u)&&ie.exec(w.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)w.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,w.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var le={};function ce(e){var t,n=e.ownerDocument,r=e.nodeName,i=le[r];return i||(t=n.body.appendChild(n.createElement(r)),i=w.css(t,"display"),t.parentNode.removeChild(t),"none"===i&&(i="block"),le[r]=i,i)}function fe(e,t){for(var n,r,i=[],o=0,a=e.length;o<a;o++)(r=e[o]).style&&(n=r.style.display,t?("none"===n&&(i[o]=J.get(r,"display")||null,i[o]||(r.style.display="")),""===r.style.display&&ae(r)&&(i[o]=ce(r))):"none"!==n&&(i[o]="none",J.set(r,"display",n)));for(o=0;o<a;o++)null!=i[o]&&(e[o].style.display=i[o]);return e}w.fn.extend({show:function(){return fe(this,!0)},hide:function(){return fe(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?w(this).show():w(this).hide()})}});var pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n<r;n++)J.set(e[n],"globalEval",!t||J.get(t[n],"globalEval"))}var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===x(o))w.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+w.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;w.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&w.inArray(o,r)>-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="<textarea>x</textarea>",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n<arguments.length;n++)u[n]=arguments[n];if(t.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,t)){s=w.event.handlers.call(this,t,l),n=0;while((o=s[n++])&&!t.isPropagationStopped()){t.currentTarget=o.elem,r=0;while((a=o.handlers[r++])&&!t.isImmediatePropagationStopped())t.rnamespace&&!t.rnamespace.test(a.namespace)||(t.handleObj=a,t.data=a.data,void 0!==(i=((w.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(t.result=i)&&(t.preventDefault(),t.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,t),t.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&e.button>=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?w(i,this).index(l)>-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(e,t){Object.defineProperty(w.Event.prototype,e,{enumerable:!0,configurable:!0,get:g(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(e){return e[w.expando]?e:new w.Event(e)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==Se()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===Se()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&N(this,"input"))return this.click(),!1},_default:function(e){return N(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},w.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},w.Event=function(e,t){if(!(this instanceof w.Event))return new w.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ee:ke,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&w.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[w.expando]=!0},w.Event.prototype={constructor:w.Event,isDefaultPrevented:ke,isPropagationStopped:ke,isImmediatePropagationStopped:ke,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ee,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ee,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ee,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},w.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&we.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&Te.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},w.event.addProp),w.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){w.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return i&&(i===r||w.contains(r,i))||(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),w.fn.extend({on:function(e,t,n,r){return De(this,e,t,n,r)},one:function(e,t,n,r){return De(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,w(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=ke),this.each(function(){w.event.remove(this,e,n,t)})}});var Ne=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/<script|<style|<link/i,je=/checked\s*(?:[^=]|=\s*.checked.)/i,qe=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n<r;n++)w.event.add(t,i,l[i][n])}K.hasData(e)&&(s=K.access(e),u=w.extend({},s),K.set(t,u))}}function Me(e,t){var n=t.nodeName.toLowerCase();"input"===n&&pe.test(e.type)?t.checked=e.checked:"input"!==n&&"textarea"!==n||(t.defaultValue=e.defaultValue)}function Re(e,t,n,r){t=a.apply([],t);var i,o,s,u,l,c,f=0,p=e.length,d=p-1,y=t[0],v=g(y);if(v||p>1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f<p;f++)l=i,f!==d&&(l=w.clone(l,!0,!0),u&&w.merge(s,ye(l,"script"))),n.call(e[f],l,f);if(u)for(c=s[s.length-1].ownerDocument,w.map(s,Oe),f=0;f<u;f++)l=s[f],he.test(l.type||"")&&!J.access(l,"globalEval")&&w.contains(c,l)&&(l.src&&"module"!==(l.type||"").toLowerCase()?w._evalUrl&&w._evalUrl(l.src):m(l.textContent.replace(qe,""),c,l))}return e}function Ie(e,t,n){for(var r,i=t?w.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||w.cleanData(ye(r)),r.parentNode&&(n&&w.contains(r.ownerDocument,r)&&ve(ye(r,"script")),r.parentNode.removeChild(r));return e}w.extend({htmlPrefilter:function(e){return e.replace(Ne,"<$1></$2>")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r<i;r++)Me(o[r],a[r]);if(t)if(n)for(o=o||ye(e),a=a||ye(s),r=0,i=o.length;r<i;r++)Pe(o[r],a[r]);else Pe(e,s);return(a=ye(s,"script")).length>0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(w.cleanData(ye(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=[];return Re(this,arguments,function(t){var n=this.parentNode;w.inArray(this,e)<0&&(w.cleanData(ye(this)),n&&n.replaceChild(t,this))},e)}}),w.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){w.fn[e]=function(e){for(var n,r=[],i=w(e),o=i.length-1,a=0;a<=o;a++)n=a===o?this:this.clone(!0),w(i[a])[t](n),s.apply(r,n.get());return this.pushStack(r)}});var We=new RegExp("^("+re+")(?!px)[a-z%]+$","i"),$e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},Be=new RegExp(oe.join("|"),"i");!function(){function t(){if(c){l.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",c.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",be.appendChild(l).appendChild(c);var t=e.getComputedStyle(c);i="1%"!==t.top,u=12===n(t.marginLeft),c.style.right="60%",s=36===n(t.right),o=36===n(t.width),c.style.position="absolute",a=36===c.offsetWidth||"absolute",be.removeChild(l),c=null}}function n(e){return Math.round(parseFloat(e))}var i,o,a,s,u,l=r.createElement("div"),c=r.createElement("div");c.style&&(c.style.backgroundClip="content-box",c.cloneNode(!0).style.backgroundClip="",h.clearCloneStyle="content-box"===c.style.backgroundClip,w.extend(h,{boxSizingReliable:function(){return t(),o},pixelBoxStyles:function(){return t(),s},pixelPosition:function(){return t(),i},reliableMarginLeft:function(){return t(),u},scrollboxSize:function(){return t(),a}}))}();function Fe(e,t,n){var r,i,o,a,s=e.style;return(n=n||$e(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||w.contains(e.ownerDocument,e)||(a=w.style(e,t)),!h.pixelBoxStyles()&&We.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function _e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}var ze=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ue={position:"absolute",visibility:"hidden",display:"block"},Ve={letterSpacing:"0",fontWeight:"400"},Ge=["Webkit","Moz","ms"],Ye=r.createElement("div").style;function Qe(e){if(e in Ye)return e;var t=e[0].toUpperCase()+e.slice(1),n=Ge.length;while(n--)if((e=Ge[n]+t)in Ye)return e}function Je(e){var t=w.cssProps[e];return t||(t=w.cssProps[e]=Qe(e)||e),t}function Ke(e,t,n){var r=ie.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ze(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=w.css(e,n+oe[a],!0,i)),r?("content"===n&&(u-=w.css(e,"padding"+oe[a],!0,i)),"margin"!==n&&(u-=w.css(e,"border"+oe[a]+"Width",!0,i))):(u+=w.css(e,"padding"+oe[a],!0,i),"padding"!==n?u+=w.css(e,"border"+oe[a]+"Width",!0,i):s+=w.css(e,"border"+oe[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a<i;a++)o[t[a]]=w.css(e,t[a],!1,r);return o}return void 0!==n?w.style(e,t,n):w.css(e,t)},e,t,arguments.length>1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function ct(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),y=J.get(e,"fxshow");n.queue||(null==(a=w._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,w.queue(e,"fx").length||a.empty.fire()})}));for(r in t)if(i=t[r],it.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!y||void 0===y[r])continue;g=!0}d[r]=y&&y[r]||w.style(e,r)}if((u=!w.isEmptyObject(t))||!w.isEmptyObject(d)){f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=y&&y.display)&&(l=J.get(e,"display")),"none"===(c=w.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=w.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===w.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1;for(r in d)u||(y?"hidden"in y&&(g=y.hidden):y=J.access(e,"fxshow",{display:l}),o&&(y.hidden=!g),g&&fe([e],!0),p.done(function(){g||fe([e]),J.remove(e,"fxshow");for(r in d)w.style(e,r,d[r])})),u=lt(g?y[r]:0,r,p),r in y||(y[r]=u.start,g&&(u.end=u.start,u.start=0))}}function ft(e,t){var n,r,i,o,a;for(n in e)if(r=G(n),i=t[r],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=w.cssHooks[r])&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function pt(e,t,n){var r,i,o=0,a=pt.prefilters.length,s=w.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;for(var t=nt||st(),n=Math.max(0,l.startTime+l.duration-t),r=1-(n/l.duration||0),o=0,a=l.tweens.length;o<a;o++)l.tweens[o].run(r);return s.notifyWith(e,[l,r,n]),r<1&&a?n:(a||s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l]),!1)},l=s.promise({elem:e,props:w.extend({},t),opts:w.extend(!0,{specialEasing:{},easing:w.easing._default},n),originalProperties:t,originalOptions:n,startTime:nt||st(),duration:n.duration,tweens:[],createTween:function(t,n){var r=w.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;n<r;n++)l.tweens[n].run(1);return t?(s.notifyWith(e,[l,1,0]),s.resolveWith(e,[l,t])):s.rejectWith(e,[l,t]),this}}),c=l.props;for(ft(c,l.opts.specialEasing);o<a;o++)if(r=pt.prefilters[o].call(l,e,c,l.opts))return g(r.stop)&&(w._queueHooks(l.elem,l.opts.queue).stop=r.stop.bind(r)),r;return w.map(c,lt,l),g(l.opts.start)&&l.opts.start.call(e,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),w.fx.timer(w.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l}w.Animation=w.extend(pt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return ue(n.elem,e,ie.exec(t),n),n}]},tweener:function(e,t){g(e)?(t=e,e=["*"]):e=e.match(M);for(var n,r=0,i=e.length;r<i;r++)n=e[r],pt.tweeners[n]=pt.tweeners[n]||[],pt.tweeners[n].unshift(t)},prefilters:[ct],prefilter:function(e,t){t?pt.prefilters.unshift(e):pt.prefilters.push(e)}}),w.speed=function(e,t,n){var r=e&&"object"==typeof e?w.extend({},e):{complete:n||!n&&t||g(e)&&e,duration:e,easing:n&&t||t&&!g(t)&&t};return w.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in w.fx.speeds?r.duration=w.fx.speeds[r.duration]:r.duration=w.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){g(r.old)&&r.old.call(this),r.queue&&w.dequeue(this,r.queue)},r},w.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=w.isEmptyObject(e),o=w.speed(t,n,r),a=function(){var t=pt(this,w.extend({},e),o);(i||J.get(this,"finish"))&&t.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&!1!==e&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=w.timers,a=J.get(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&ot.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));!t&&n||w.dequeue(this,e)})},finish:function(e){return!1!==e&&(e=e||"fx"),this.each(function(){var t,n=J.get(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=w.timers,a=r?r.length:0;for(n.finish=!0,w.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;t<a;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),w.each(["toggle","show","hide"],function(e,t){var n=w.fn[t];w.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ut(t,!0),e,r,i)}}),w.each({slideDown:ut("show"),slideUp:ut("hide"),slideToggle:ut("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){w.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),w.timers=[],w.fx.tick=function(){var e,t=0,n=w.timers;for(nt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||w.fx.stop(),nt=void 0},w.fx.timer=function(e){w.timers.push(e),w.fx.start()},w.fx.interval=13,w.fx.start=function(){rt||(rt=!0,at())},w.fx.stop=function(){rt=null},w.fx.speeds={slow:600,fast:200,_default:400},w.fn.delay=function(t,n){return t=w.fx?w.fx.speeds[t]||t:t,n=n||"fx",this.queue(n,function(n,r){var i=e.setTimeout(n,t);r.stop=function(){e.clearTimeout(i)}})},function(){var e=r.createElement("input"),t=r.createElement("select").appendChild(r.createElement("option"));e.type="checkbox",h.checkOn=""!==e.value,h.optSelected=t.selected,(e=r.createElement("input")).value="t",e.type="radio",h.radioValue="t"===e.value}();var dt,ht=w.expr.attrHandle;w.fn.extend({attr:function(e,t){return z(this,w.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!N(n.parentNode,"optgroup"))){if(t=w(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=w.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=w.inArray(w.valHooks.option.get(r),o)>-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("<script>").prop({charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&o("error"===e.type?404:200,e.type)}),r.head.appendChild(t[0])},abort:function(){n&&n()}}}});var Yt=[],Qt=/(=)\?(?=&|$)|\?\?/;w.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Yt.pop()||w.expando+"_"+Et++;return this[e]=!0,e}}),w.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(Qt.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Qt.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=g(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Qt,"$1"+i):!1!==t.jsonp&&(t.url+=(kt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||w.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?w(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,Yt.push(i)),a&&g(o)&&o(a[0]),a=o=void 0}),"script"}),h.createHTMLDocument=function(){var e=r.implementation.createHTMLDocument("").body;return e.innerHTML="<form></form><form></form>",2===e.childNodes.length}(),w.parseHTML=function(e,t,n){if("string"!=typeof e)return[];"boolean"==typeof t&&(n=t,t=!1);var i,o,a;return t||(h.createHTMLDocument?((i=(t=r.implementation.createHTMLDocument("")).createElement("base")).href=r.location.href,t.head.appendChild(i)):t=r),o=A.exec(e),a=!n&&[],o?[t.createElement(o[1])]:(o=xe([e],t,a),a&&a.length&&w(a).remove(),w.merge([],o.childNodes))},w.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=vt(e.slice(s)),e=e.slice(0,s)),g(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&w.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?w("<div>").append(w.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},w.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){w.fn[t]=function(e){return this.on(t,e)}}),w.expr.pseudos.animated=function(e){return w.grep(w.timers,function(t){return e===t.elem}).length},w.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l,c=w.css(e,"position"),f=w(e),p={};"static"===c&&(e.style.position="relative"),s=f.offset(),o=w.css(e,"top"),u=w.css(e,"left"),(l=("absolute"===c||"fixed"===c)&&(o+u).indexOf("auto")>-1)?(a=(r=f.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),g(t)&&(t=t.call(e,n,w.extend({},s))),null!=t.top&&(p.top=t.top-s.top+a),null!=t.left&&(p.left=t.left-s.left+i),"using"in t?t.using.call(e,p):f.css(p)}},w.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){w.offset.setOffset(this,e,t)});var t,n,r=this[0];if(r)return r.getClientRects().length?(t=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===w.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===w.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=w(e).offset()).top+=w.css(e,"borderTopWidth",!0),i.left+=w.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-w.css(r,"marginTop",!0),left:t.left-i.left-w.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===w.css(e,"position"))e=e.offsetParent;return e||be})}}),w.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n="pageYOffset"===t;w.fn[e]=function(r){return z(this,function(e,r,i){var o;if(y(e)?o=e:9===e.nodeType&&(o=e.defaultView),void 0===i)return o?o[t]:e[r];o?o.scrollTo(n?o.pageXOffset:i,n?i:o.pageYOffset):e[r]=i},e,r,arguments.length)}}),w.each(["top","left"],function(e,t){w.cssHooks[t]=_e(h.pixelPosition,function(e,n){if(n)return n=Fe(e,t),We.test(n)?w(e).position()[t]+"px":n})}),w.each({Height:"height",Width:"width"},function(e,t){w.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){w.fn[r]=function(i,o){var a=arguments.length&&(n||"boolean"!=typeof i),s=n||(!0===i||!0===o?"margin":"border");return z(this,function(t,n,i){var o;return y(t)?0===r.indexOf("outer")?t["inner"+e]:t.document.documentElement["client"+e]:9===t.nodeType?(o=t.documentElement,Math.max(t.body["scroll"+e],o["scroll"+e],t.body["offset"+e],o["offset"+e],o["client"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),w.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},"function"==typeof define&&define.amd&&define("jquery",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});
diff --git a/system/javascript/osapjs/client/libs/math.js b/system/javascript/osapjs/client/libs/math.js
new file mode 100644
index 0000000000000000000000000000000000000000..e26eb46136ee3355ece403780322ca7d8bd5def3
--- /dev/null
+++ b/system/javascript/osapjs/client/libs/math.js
@@ -0,0 +1,56 @@
+/**
+ * math.js
+ * https://github.com/josdejong/mathjs
+ *
+ * Math.js is an extensive math library for JavaScript and Node.js,
+ * It features real and complex numbers, units, matrices, a large set of
+ * mathematical functions, and a flexible expression parser.
+ *
+ * @version 6.2.3
+ * @date    2019-10-06
+ *
+ * @license
+ * Copyright (C) 2013-2019 Jos de Jong <wjosdejong@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.math=t():e.math=t()}(this,function(){return n={},i.m=r=[function(e,t,r){"use strict";r.d(t,"a",function(){return n}),r.d(t,"b",function(){return i}),r.d(t,"c",function(){return o});r(2);var a=r(3);function n(r,n,i,e){function t(e){var t=Object(a.j)(e,n.map(o));return function(e,t,r){if(!t.filter(function(e){return!function(e){return e&&"?"===e[0]}(e)}).every(function(e){return void 0!==r[e]})){var n=t.filter(function(e){return void 0===r[e]});throw new Error('Cannot create function "'.concat(e,'", ')+"some dependencies are missing: ".concat(n.map(function(e){return'"'.concat(e,'"')}).join(", "),"."))}}(r,n,e),i(t)}return t.isFactory=!0,t.fn=r,t.dependencies=n.slice().sort(),e&&(t.meta=e),t}function i(e){return"function"==typeof e&&"string"==typeof e.fn&&Array.isArray(e.dependencies)}function o(e){return e&&"?"===e[0]?e.slice(1):e}},function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e){return"number"==typeof e}function a(e){return e&&!0===e.constructor.prototype.isBigNumber||!1}function o(e){return e&&"object"===n(e)&&!0===Object.getPrototypeOf(e).isComplex||!1}function s(e){return e&&"object"===n(e)&&!0===Object.getPrototypeOf(e).isFraction||!1}function u(e){return e&&!0===e.constructor.prototype.isUnit||!1}function c(e){return"string"==typeof e}r.d(t,"y",function(){return i}),r.d(t,"e",function(){return a}),r.d(t,"j",function(){return o}),r.d(t,"o",function(){return s}),r.d(t,"L",function(){return u}),r.d(t,"I",function(){return c}),r.d(t,"b",function(){return f}),r.d(t,"v",function(){return l}),r.d(t,"i",function(){return p}),r.d(t,"n",function(){return m}),r.d(t,"H",function(){return h}),r.d(t,"D",function(){return d}),r.d(t,"t",function(){return y}),r.d(t,"g",function(){return g}),r.d(t,"G",function(){return v}),r.d(t,"s",function(){return b}),r.d(t,"p",function(){return x}),r.d(t,"m",function(){return w}),r.d(t,"F",function(){return N}),r.d(t,"z",function(){return O}),r.d(t,"x",function(){return M}),r.d(t,"K",function(){return E}),r.d(t,"a",function(){return j}),r.d(t,"c",function(){return S}),r.d(t,"d",function(){return A}),r.d(t,"f",function(){return C}),r.d(t,"k",function(){return T}),r.d(t,"l",function(){return _}),r.d(t,"q",function(){return I}),r.d(t,"r",function(){return q}),r.d(t,"u",function(){return B}),r.d(t,"w",function(){return k}),r.d(t,"A",function(){return z}),r.d(t,"B",function(){return D}),r.d(t,"C",function(){return R}),r.d(t,"E",function(){return P}),r.d(t,"J",function(){return F}),r.d(t,"h",function(){return U}),r.d(t,"M",function(){return L});var f=Array.isArray;function l(e){return e&&!0===e.constructor.prototype.isMatrix||!1}function p(e){return Array.isArray(e)||l(e)}function m(e){return e&&e.isDenseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function h(e){return e&&e.isSparseMatrix&&!0===e.constructor.prototype.isMatrix||!1}function d(e){return e&&!0===e.constructor.prototype.isRange||!1}function y(e){return e&&!0===e.constructor.prototype.isIndex||!1}function g(e){return"boolean"==typeof e}function v(e){return e&&!0===e.constructor.prototype.isResultSet||!1}function b(e){return e&&!0===e.constructor.prototype.isHelp||!1}function x(e){return"function"==typeof e}function w(e){return e instanceof Date}function N(e){return e instanceof RegExp}function O(e){return!(!e||"object"!==n(e)||e.constructor!==Object||o(e)||s(e))}function M(e){return null===e}function E(e){return void 0===e}function j(e){return e&&!0===e.isAccessorNode&&!0===e.constructor.prototype.isNode||!1}function S(e){return e&&!0===e.isArrayNode&&!0===e.constructor.prototype.isNode||!1}function A(e){return e&&!0===e.isAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function C(e){return e&&!0===e.isBlockNode&&!0===e.constructor.prototype.isNode||!1}function T(e){return e&&!0===e.isConditionalNode&&!0===e.constructor.prototype.isNode||!1}function _(e){return e&&!0===e.isConstantNode&&!0===e.constructor.prototype.isNode||!1}function I(e){return e&&!0===e.isFunctionAssignmentNode&&!0===e.constructor.prototype.isNode||!1}function q(e){return e&&!0===e.isFunctionNode&&!0===e.constructor.prototype.isNode||!1}function B(e){return e&&!0===e.isIndexNode&&!0===e.constructor.prototype.isNode||!1}function k(e){return e&&!0===e.isNode&&!0===e.constructor.prototype.isNode||!1}function z(e){return e&&!0===e.isObjectNode&&!0===e.constructor.prototype.isNode||!1}function D(e){return e&&!0===e.isOperatorNode&&!0===e.constructor.prototype.isNode||!1}function R(e){return e&&!0===e.isParenthesisNode&&!0===e.constructor.prototype.isNode||!1}function P(e){return e&&!0===e.isRangeNode&&!0===e.constructor.prototype.isNode||!1}function F(e){return e&&!0===e.isSymbolNode&&!0===e.constructor.prototype.isNode||!1}function U(e){return e&&!0===e.constructor.prototype.isChain||!1}function L(e){var t=n(e);return"object"===t?null===e?"null":Array.isArray(e)?"Array":e instanceof Date?"Date":e instanceof RegExp?"RegExp":a(e)?"BigNumber":o(e)?"Complex":s(e)?"Fraction":l(e)?"Matrix":u(e)?"Unit":y(e)?"Index":d(e)?"Range":v(e)?"ResultSet":k(e)?e.type:U(e)?"Chain":b(e)?"Help":"Object":"function"===t?"Function":t}},function(e,t,r){"use strict";r.d(t,"a",function(){return s}),r.d(t,"r",function(){return c}),r.d(t,"s",function(){return f}),r.d(t,"o",function(){return l}),r.d(t,"n",function(){return p}),r.d(t,"p",function(){return m}),r.d(t,"q",function(){return h}),r.d(t,"e",function(){return d}),r.d(t,"m",function(){return y}),r.d(t,"f",function(){return g}),r.d(t,"c",function(){return v}),r.d(t,"d",function(){return b}),r.d(t,"k",function(){return x}),r.d(t,"i",function(){return w}),r.d(t,"g",function(){return N}),r.d(t,"h",function(){return O}),r.d(t,"l",function(){return M}),r.d(t,"j",function(){return E}),r.d(t,"b",function(){return j});var n=r(4),i=r(1),a=r(5),u=r(6),o=r(10);function s(e){for(var t=[];Array.isArray(e);)t.push(e.length),e=e[0];return t}function c(e,t){if(0===t.length){if(Array.isArray(e))throw new u.a(e.length,0)}else!function e(t,r,n){var i,a=t.length;if(a!==r[n])throw new u.a(a,r[n]);if(n<r.length-1){var o=n+1;for(i=0;i<a;i++){var s=t[i];if(!Array.isArray(s))throw new u.a(r.length-1,r.length,"<");e(t[i],r,o)}}else for(i=0;i<a;i++)if(Array.isArray(t[i]))throw new u.a(r.length+1,r.length,">")}(e,t,0)}function f(e,t){if(!Object(i.y)(e)||!Object(n.i)(e))throw new TypeError("Index must be an integer (value: "+e+")");if(e<0||"number"==typeof t&&t<=e)throw new o.a(e,t)}function l(e,t,r){if(!Array.isArray(e)||!Array.isArray(t))throw new TypeError("Array expected");if(0===t.length)throw new Error("Resizing to scalar is not supported");return t.forEach(function(e){if(!Object(i.y)(e)||!Object(n.i)(e)||e<0)throw new TypeError("Invalid size, must contain positive integers (size: "+Object(a.d)(t)+")")}),function e(t,r,n,i){var a;var o;var s=t.length;var u=r[n];var c=Math.min(s,u);t.length=u;if(n<r.length-1){var f=n+1;for(a=0;a<c;a++)o=t[a],Array.isArray(o)||(o=[o],t[a]=o),e(o,r,f,i);for(a=c;a<u;a++)o=[],t[a]=o,e(o,r,f,i)}else{for(a=0;a<c;a++)for(;Array.isArray(t[a]);)t[a]=t[a][0];for(a=c;a<u;a++)t[a]=i}}(e,t,0,void 0!==r?r:0),e}function p(t,r){var e,n=d(t);function i(e){return e.reduce(function(e,t){return e*t})}if(!Array.isArray(t)||!Array.isArray(r))throw new TypeError("Array expected");if(0===r.length)throw new u.a(0,i(s(t)),"!=");for(var a=1,o=0;o<r.length;o++)a*=r[o];if(n.length!==a)throw new u.a(i(r),i(s(t)),"!=");try{e=function(e,t){for(var r,n=e,i=t.length-1;0<i;i--){var a=t[i];r=[];for(var o=n.length/a,s=0;s<o;s++)r.push(n.slice(s*a,(s+1)*a));n=r}return n}(n,r)}catch(e){if(e instanceof u.a)throw new u.a(i(r),i(s(t)),"!=");throw e}return e}function m(e,t){for(var r=t||s(e);Array.isArray(e)&&1===e.length;)e=e[0],r.shift();for(var n=r.length;1===r[n-1];)n--;return n<r.length&&(e=function e(t,r,n){var i,a;if(n<r){var o=n+1;for(i=0,a=t.length;i<a;i++)t[i]=e(t[i],r,o)}else for(;Array.isArray(t);)t=t[0];return t}(e,n,0),r.length=n),e}function h(e,t,r,n){var i=n||s(e);if(r)for(var a=0;a<r;a++)e=[e],i.unshift(1);for(e=function e(t,r,n){var i,a;if(Array.isArray(t)){var o=n+1;for(i=0,a=t.length;i<a;i++)t[i]=e(t[i],r,o)}else for(var s=n;s<r;s++)t=[t];return t}(e,t,0);i.length<t;)i.push(1);return e}function d(e){if(!Array.isArray(e))return e;var r=[];return e.forEach(function e(t){Array.isArray(t)?t.forEach(e):r.push(t)}),r}function y(e,t){return Array.prototype.map.call(e,t)}function g(e,t){Array.prototype.forEach.call(e,t)}function v(e,t){if(1!==s(e).length)throw new Error("Only one dimensional matrices supported");return Array.prototype.filter.call(e,t)}function b(e,t){if(1!==s(e).length)throw new Error("Only one dimensional matrices supported");return Array.prototype.filter.call(e,function(e){return t.test(e)})}function x(e,t){return Array.prototype.join.call(e,t)}function w(e){if(!Array.isArray(e))throw new TypeError("Array input expected");if(0===e.length)return e;var t=[],r=0;t[0]={value:e[0],identifier:0};for(var n=1;n<e.length;n++)e[n]===e[n-1]?r++:r=0,t.push({value:e[n],identifier:r});return t}function N(e){if(!Array.isArray(e))throw new TypeError("Array input expected");if(0===e.length)return e;for(var t=[],r=0;r<e.length;r++)t.push(e[r].value);return t}function O(e,t){for(var r,n=0,i=0;i<e.length;i++){var a=e[i],o=Array.isArray(a);if(0===i&&o&&(n=a.length),o&&a.length!==n)return;var s=o?O(a,t):t(a);if(void 0===r)r=s;else if(r!==s)return"mixed"}return r}function M(e){return e[e.length-1]}function E(e){return e.slice(0,e.length-1)}function j(e,t){return-1!==e.indexOf(t)}},function(e,t,r){"use strict";r.d(t,"a",function(){return i}),r.d(t,"i",function(){return o}),r.d(t,"e",function(){return s}),r.d(t,"b",function(){return u}),r.d(t,"d",function(){return c}),r.d(t,"c",function(){return f}),r.d(t,"h",function(){return l}),r.d(t,"k",function(){return p}),r.d(t,"f",function(){return m}),r.d(t,"g",function(){return h}),r.d(t,"j",function(){return d}),r.d(t,"l",function(){return y});var n=r(1);function a(e){return(a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e){var t=a(e);if("number"===t||"string"===t||"boolean"===t||null==e)return e;if("function"==typeof e.clone)return e.clone();if(Array.isArray(e))return e.map(function(e){return i(e)});if(e instanceof Date)return new Date(e.valueOf());if(Object(n.e)(e))return e;if(e instanceof RegExp)throw new TypeError("Cannot clone "+e);return o(e,i)}function o(e,t){var r={};for(var n in e)m(e,n)&&(r[n]=t(e[n]));return r}function s(e,t){for(var r in t)m(t,r)&&(e[r]=t[r]);return e}function u(e,t){if(Array.isArray(t))throw new TypeError("Arrays are not supported by deepExtend");for(var r in t)if(m(t,r))if(t[r]&&t[r].constructor===Object)void 0===e[r]&&(e[r]={}),e[r]&&e[r].constructor===Object?u(e[r],t[r]):e[r]=t[r];else{if(Array.isArray(t[r]))throw new TypeError("Arrays are not supported by deepExtend");e[r]=t[r]}return e}function c(e,t){var r,n,i;if(Array.isArray(e)){if(!Array.isArray(t))return!1;if(e.length!==t.length)return!1;for(n=0,i=e.length;n<i;n++)if(!c(e[n],t[n]))return!1;return!0}if("function"==typeof e)return e===t;if(e instanceof Object){if(Array.isArray(t)||!(t instanceof Object))return!1;for(r in e)if(!(r in t&&c(e[r],t[r])))return!1;for(r in t)if(!(r in e&&c(e[r],t[r])))return!1;return!0}return e===t}function f(e){var t={};return function e(t,r){for(var n in t)if(m(t,n)){var i=t[n];"object"===a(i)&&null!==i?e(i,r):r[n]=i}}(e,t),t}function l(e,t,r){var n,i=!0;Object.defineProperty(e,t,{get:function(){return i&&(n=r(),i=!1),n},set:function(e){n=e,i=!1},configurable:!0,enumerable:!0})}function p(e,t){if(t&&"string"==typeof t)return p(e,t.split("."));var r=e;if(t)for(var n=0;n<t.length;n++){var i=t[n];i in r||(r[i]={}),r=r[i]}return r}function m(e,t){return e&&Object.hasOwnProperty.call(e,t)}function h(e){return e&&"function"==typeof e.factory}function d(e,t){for(var r={},n=0;n<t.length;n++){var i=t[n],a=e[i];void 0!==a&&(r[i]=a)}return r}function y(t){return Object.keys(t).map(function(e){return t[e]})}},function(e,t,r){"use strict";r.d(t,"i",function(){return n}),r.d(t,"n",function(){return i}),r.d(t,"l",function(){return o}),r.d(t,"j",function(){return s}),r.d(t,"k",function(){return u}),r.d(t,"d",function(){return c}),r.d(t,"g",function(){return f}),r.d(t,"h",function(){return l}),r.d(t,"q",function(){return h}),r.d(t,"f",function(){return b}),r.d(t,"m",function(){return w}),r.d(t,"a",function(){return N}),r.d(t,"b",function(){return O}),r.d(t,"c",function(){return M}),r.d(t,"e",function(){return E}),r.d(t,"o",function(){return j}),r.d(t,"p",function(){return S});var a=r(3),p=r(1);function n(e){return"boolean"==typeof e||!!isFinite(e)&&e===Math.round(e)}var i=Math.sign||function(e){return 0<e?1:e<0?-1:0},o=Math.log2||function(e){return Math.log(e)/Math.LN2},s=Math.log10||function(e){return Math.log(e)/Math.LN10},u=Math.log1p||function(e){return Math.log(e+1)},c=Math.cbrt||function(e){if(0===e)return e;var t,r=e<0;return r&&(e=-e),t=isFinite(e)?(e/((t=Math.exp(Math.log(e)/3))*t)+2*t)/3:e,r?-t:t},f=Math.expm1||function(e){return 2e-4<=e||e<=-2e-4?Math.exp(e)-1:e+e*e/2+e*e*e/6};function l(e,t){if("function"==typeof t)return t(e);if(e===1/0)return"Infinity";if(e===-1/0)return"-Infinity";if(isNaN(e))return"NaN";var r,n="auto";switch(t&&(t.notation&&(n=t.notation),Object(p.y)(t)?r=t:Object(p.y)(t.precision)&&(r=t.precision)),n){case"fixed":return h(e,r);case"exponential":return d(e,r);case"engineering":return function(e,t){if(isNaN(e)||!isFinite(e))return String(e);var r=g(m(e),t),n=r.exponent,i=r.coefficients,a=n%3==0?n:n<0?n-3-n%3:n-n%3;if(Object(p.y)(t))for(;t>i.length||n-a+1>i.length;)i.push(0);else for(var o=0<=n?n:Math.abs(a);i.length-1<o;)i.push(0);var s=Math.abs(n-a),u=1;for(;0<s;)u++,s--;var c=i.slice(u).join(""),f=Object(p.y)(t)&&c.length||c.match(/[1-9]/)?"."+c:"",l=i.slice(0,u).join("")+f+"e"+(0<=n?"+":"")+a.toString();return r.sign+l}(e,r);case"auto":if(!t||!t.exponential||void 0===t.exponential.lower&&void 0===t.exponential.upper)return y(e,r,t&&t).replace(/((\.\d*?)(0+))($|e)/,function(){var e=arguments[2],t=arguments[4];return"."!==e?e+t:t});var i=Object(a.i)(t,function(e){return e});return(i.exponential=void 0)!==t.exponential.lower&&(i.lowerExp=Math.round(Math.log(t.exponential.lower)/Math.LN10)),void 0!==t.exponential.upper&&(i.upperExp=Math.round(Math.log(t.exponential.upper)/Math.LN10)),console.warn("Deprecation warning: Formatting options exponential.lower and exponential.upper (minimum and maximum value) are replaced with exponential.lowerExp and exponential.upperExp (minimum and maximum exponent) since version 4.0.0. Replace "+JSON.stringify(t)+" with "+JSON.stringify(i)),y(e,r,i);default:throw new Error('Unknown notation "'+n+'". Choose "auto", "exponential", or "fixed".')}}function m(e){var t=String(e).toLowerCase().match(/^0*?(-?)(\d+\.?\d*)(e([+-]?\d+))?$/);if(!t)throw new SyntaxError("Invalid number "+e);var r=t[1],n=t[2],i=parseFloat(t[4]||"0"),a=n.indexOf(".");i+=-1!==a?a-1:n.length-1;var o=n.replace(".","").replace(/^0*/,function(e){return i-=e.length,""}).replace(/0*$/,"").split("").map(function(e){return parseInt(e)});return 0===o.length&&(o.push(0),i++),{sign:r,coefficients:o,exponent:i}}function h(e,t){if(isNaN(e)||!isFinite(e))return String(e);var r=m(e),n="number"==typeof t?g(r,r.exponent+1+t):r,i=n.coefficients,a=n.exponent+1,o=a+(t||0);return i.length<o&&(i=i.concat(v(o-i.length))),a<0&&(i=v(1-a).concat(i),a=1),a<i.length&&i.splice(a,0,0===a?"0.":"."),n.sign+i.join("")}function d(e,t){if(isNaN(e)||!isFinite(e))return String(e);var r=m(e),n=t?g(r,t):r,i=n.coefficients,a=n.exponent;i.length<t&&(i=i.concat(v(t-i.length)));var o=i.shift();return n.sign+o+(0<i.length?"."+i.join(""):"")+"e"+(0<=a?"+":"")+a}function y(e,t,r){if(isNaN(e)||!isFinite(e))return String(e);var n=r&&void 0!==r.lowerExp?r.lowerExp:-3,i=r&&void 0!==r.upperExp?r.upperExp:5,a=m(e),o=t?g(a,t):a;if(o.exponent<n||o.exponent>=i)return d(e,t);var s=o.coefficients,u=o.exponent;s.length<t&&(s=s.concat(v(t-s.length))),s=s.concat(v(u-s.length+1+(s.length<t?t-s.length:0)));var c=0<u?u:0;return c<(s=v(-u).concat(s)).length-1&&s.splice(c+1,0,"."),o.sign+s.join("")}function g(e,t){for(var r={sign:e.sign,coefficients:e.coefficients,exponent:e.exponent},n=r.coefficients;t<=0;)n.unshift(0),r.exponent++,t++;if(n.length>t&&5<=n.splice(t,n.length-t)[0]){var i=t-1;for(n[i]++;10===n[i];)n.pop(),0===i&&(n.unshift(0),r.exponent++,i++),n[--i]++}return r}function v(e){for(var t=[],r=0;r<e;r++)t.push(0);return t}function b(e){return e.toExponential().replace(/e.*$/,"").replace(/^0\.?0*|\./,"").length}var x=Number.EPSILON||2220446049250313e-31;function w(e,t,r){if(null==r)return e===t;if(e===t)return!0;if(isNaN(e)||isNaN(t))return!1;if(isFinite(e)&&isFinite(t)){var n=Math.abs(e-t);return n<x||n<=Math.max(Math.abs(e),Math.abs(t))*r}return!1}var N=Math.acosh||function(e){return Math.log(Math.sqrt(e*e-1)+e)},O=Math.asinh||function(e){return Math.log(Math.sqrt(e*e+1)+e)},M=Math.atanh||function(e){return Math.log((1+e)/(1-e))/2},E=Math.cosh||function(e){return(Math.exp(e)+Math.exp(-e))/2},j=Math.sinh||function(e){return(Math.exp(e)-Math.exp(-e))/2},S=Math.tanh||function(e){var t=Math.exp(2*e);return(t-1)/(t+1)}},function(e,t,r){"use strict";var n=r(1),i=r(4),c=r(3);function f(e,t){if("function"==typeof t)return t(e);if(!e.isFinite())return e.isNaN()?"NaN":e.gt(0)?"Infinity":"-Infinity";var r,n="auto";switch(void 0!==t&&(t.notation&&(n=t.notation),"number"==typeof t?r=t:t.precision&&(r=t.precision)),n){case"fixed":return function(e,t){return e.toFixed(t)}(e,r);case"exponential":return l(e,r);case"engineering":return function(e,t){var r=e.e,n=r%3==0?r:r<0?r-3-r%3:r-r%3,i=e.mul(Math.pow(10,-n)),a=i.toPrecision(t);-1!==a.indexOf("e")&&(a=i.toString());return a+"e"+(0<=r?"+":"")+n.toString()}(e,r);case"auto":if(t&&t.exponential&&(void 0!==t.exponential.lower||void 0!==t.exponential.upper)){var i=Object(c.i)(t,function(e){return e});return(i.exponential=void 0)!==t.exponential.lower&&(i.lowerExp=Math.round(Math.log(t.exponential.lower)/Math.LN10)),void 0!==t.exponential.upper&&(i.upperExp=Math.round(Math.log(t.exponential.upper)/Math.LN10)),console.warn("Deprecation warning: Formatting options exponential.lower and exponential.upper (minimum and maximum value) are replaced with exponential.lowerExp and exponential.upperExp (minimum and maximum exponent) since version 4.0.0. Replace "+JSON.stringify(t)+" with "+JSON.stringify(i)),f(e,i)}var a=t&&void 0!==t.lowerExp?t.lowerExp:-3,o=t&&void 0!==t.upperExp?t.upperExp:5;if(e.isZero())return"0";var s=e.toSignificantDigits(r),u=s.e;return(a<=u&&u<o?s.toFixed():l(e,r)).replace(/((\.\d*?)(0+))($|e)/,function(){var e=arguments[2],t=arguments[4];return"."!==e?e+t:t});default:throw new Error('Unknown notation "'+n+'". Choose "auto", "exponential", or "fixed".')}}function l(e,t){return void 0!==t?e.toExponential(t-1):e.toExponential()}function a(e){return(a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(e,t){var r=e.length-t.length,n=e.length;return e.substring(r,n)===t}function s(t,r){return"number"==typeof t?Object(i.h)(t,r):Object(n.e)(t)?f(t,r):function(e){return e&&"object"===a(e)&&"number"==typeof e.s&&"number"==typeof e.n&&"number"==typeof e.d||!1}(t)?r&&"decimal"===r.fraction?t.toString():t.s*t.n+"/"+t.d:Array.isArray(t)?function e(t,r){{if(Array.isArray(t)){for(var n="[",i=t.length,a=0;a<i;a++)0!==a&&(n+=", "),n+=e(t[a],r);return n+="]"}return s(t,r)}}(t,r):Object(n.I)(t)?'"'+t+'"':"function"==typeof t?t.syntax?String(t.syntax):"function":t&&"object"===a(t)?"function"==typeof t.format?t.format(r):t&&t.toString(r)!=={}.toString()?t.toString(r):"{"+Object.keys(t).map(function(e){return'"'+e+'": '+s(t[e],r)}).join(", ")+"}":String(t)}function u(e){for(var t=String(e),r="",n=0;n<t.length;){var i=t.charAt(n);"\\"===i?(r+=i,n++,""!==(i=t.charAt(n))&&-1!=='"\\/bfnrtu'.indexOf(i)||(r+="\\"),r+=i):r+='"'===i?'\\"':i,n++}return'"'+r+'"'}function p(e){var t=String(e);return t=t.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function m(e,t){if(!Object(n.I)(e))throw new TypeError("Unexpected type of argument in function compareText (expected: string or Array or Matrix, actual: "+Object(n.M)(e)+", index: 0)");if(!Object(n.I)(t))throw new TypeError("Unexpected type of argument in function compareText (expected: string or Array or Matrix, actual: "+Object(n.M)(t)+", index: 1)");return e===t?0:t<e?1:-1}r.d(t,"b",function(){return o}),r.d(t,"d",function(){return s}),r.d(t,"e",function(){return u}),r.d(t,"c",function(){return p}),r.d(t,"a",function(){return m})},function(e,t,r){"use strict";function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");this.actual=e,this.expected=t,this.relation=r,this.message="Dimension mismatch ("+(Array.isArray(e)?"["+e.join(", ")+"]":e)+" "+(this.relation||"!=")+" "+(Array.isArray(t)?"["+t.join(", ")+"]":t)+")",this.stack=(new Error).stack}r.d(t,"a",function(){return n}),(n.prototype=new RangeError).constructor=RangeError,n.prototype.name="DimensionError",n.prototype.isDimensionError=!0},,function(e,t,r){"use strict";r.d(t,"a",function(){return n});var a,n=(a={},function(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];var n,i=t.join(", ");a[i]||(a[i]=!0,(n=console).warn.apply(n,["Warning:"].concat(t)))})},function(e,t,r){var o;
+/**
+ * @license Complex.js v2.0.11 11/02/2016
+ *
+ * Copyright (c) 2016, Robert Eisele (robert@xarg.org)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ **/
+/**
+ * @license Complex.js v2.0.11 11/02/2016
+ *
+ * Copyright (c) 2016, Robert Eisele (robert@xarg.org)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ **/
+!function(){"use strict";function n(e){return.5*(Math.exp(e)+Math.exp(-e))}function i(e){return.5*(Math.exp(e)-Math.exp(-e))}function u(){throw SyntaxError("Invalid Param")}function c(e,t){var r=Math.abs(e),n=Math.abs(t);return 0===e?Math.log(n):0===t?Math.log(r):r<3e3&&n<3e3?.5*Math.log(e*e+t*t):Math.log(e/Math.cos(Math.atan2(t,e)))}var a=function(e,t){var r={re:0,im:0};if(null==e)r.re=r.im=0;else if(void 0!==t)r.re=e,r.im=t;else switch(typeof e){case"object":if("im"in e&&"re"in e)r.re=e.re,r.im=e.im;else if("abs"in e&&"arg"in e){if(!Number.isFinite(e.abs)&&Number.isFinite(e.arg))return f.INFINITY;r.re=e.abs*Math.cos(e.arg),r.im=e.abs*Math.sin(e.arg)}else if("r"in e&&"phi"in e){if(!Number.isFinite(e.r)&&Number.isFinite(e.phi))return f.INFINITY;r.re=e.r*Math.cos(e.phi),r.im=e.r*Math.sin(e.phi)}else 2===e.length?(r.re=e[0],r.im=e[1]):u();break;case"string":r.im=r.re=0;var n=e.match(/\d+\.?\d*e[+-]?\d+|\d+\.?\d*|\.\d+|./g),i=1,a=0;null===n&&u();for(var o=0;o<n.length;o++){var s=n[o];" "===s||"\t"===s||"\n"===s||("+"===s?i++:"-"===s?a++:i=a=("i"===s||"I"===s?(i+a===0&&u()," "===n[o+1]||isNaN(n[o+1])?r.im+=parseFloat((a%2?"-":"")+"1"):(r.im+=parseFloat((a%2?"-":"")+n[o+1]),o++)):(i+a!==0&&!isNaN(s)||u(),"i"===n[o+1]||"I"===n[o+1]?(r.im+=parseFloat((a%2?"-":"")+s),o++):r.re+=parseFloat((a%2?"-":"")+s)),0))}0<i+a&&u();break;case"number":r.im=0,r.re=e;break;default:u()}return isNaN(r.re)||isNaN(r.im),r};function f(e,t){if(!(this instanceof f))return new f(e,t);var r=a(e,t);this.re=r.re,this.im=r.im}f.prototype={re:0,im:0,sign:function(){var e=this.abs();return new f(this.re/e,this.im/e)},add:function(e,t){var r=new f(e,t);return this.isInfinite()&&r.isInfinite()?f.NAN:this.isInfinite()||r.isInfinite()?f.INFINITY:new f(this.re+r.re,this.im+r.im)},sub:function(e,t){var r=new f(e,t);return this.isInfinite()&&r.isInfinite()?f.NAN:this.isInfinite()||r.isInfinite()?f.INFINITY:new f(this.re-r.re,this.im-r.im)},mul:function(e,t){var r=new f(e,t);return this.isInfinite()&&r.isZero()||this.isZero()&&r.isInfinite()?f.NAN:this.isInfinite()||r.isInfinite()?f.INFINITY:0===r.im&&0===this.im?new f(this.re*r.re,0):new f(this.re*r.re-this.im*r.im,this.re*r.im+this.im*r.re)},div:function(e,t){var r=new f(e,t);if(this.isZero()&&r.isZero()||this.isInfinite()&&r.isInfinite())return f.NAN;if(this.isInfinite()||r.isZero())return f.INFINITY;if(this.isZero()||r.isInfinite())return f.ZERO;e=this.re,t=this.im;var n,i,a=r.re,o=r.im;return 0===o?new f(e/a,t/a):Math.abs(a)<Math.abs(o)?new f((e*(i=a/o)+t)/(n=a*i+o),(t*i-e)/n):new f((e+t*(i=o/a))/(n=o*i+a),(t-e*i)/n)},pow:function(e,t){var r=new f(e,t);if(e=this.re,t=this.im,r.isZero())return f.ONE;if(0===r.im){if(0===t&&0<=e)return new f(Math.pow(e,r.re),0);if(0===e)switch((r.re%4+4)%4){case 0:return new f(Math.pow(t,r.re),0);case 1:return new f(0,Math.pow(t,r.re));case 2:return new f(-Math.pow(t,r.re),0);case 3:return new f(0,-Math.pow(t,r.re))}}if(0===e&&0===t&&0<r.re&&0<=r.im)return f.ZERO;var n=Math.atan2(t,e),i=c(e,t);return e=Math.exp(r.re*i-r.im*n),t=r.im*i+r.re*n,new f(e*Math.cos(t),e*Math.sin(t))},sqrt:function(){var e,t,r=this.re,n=this.im,i=this.abs();if(0<=r){if(0===n)return new f(Math.sqrt(r),0);e=.5*Math.sqrt(2*(i+r))}else e=Math.abs(n)/Math.sqrt(2*(i-r));return t=r<=0?.5*Math.sqrt(2*(i-r)):Math.abs(n)/Math.sqrt(2*(i+r)),new f(e,n<0?-t:t)},exp:function(){var e=Math.exp(this.re);return this.im,new f(e*Math.cos(this.im),e*Math.sin(this.im))},expm1:function(){var e=this.re,t=this.im;return new f(Math.expm1(e)*Math.cos(t)+function(e){var t=Math.PI/4;if(e<-t||t<e)return Math.cos(e)-1;var r=e*e;return r*(r*(1/24+r*(-1/720+r*(1/40320+r*(-1/3628800+r*(1/4790014600+r*(-1/87178291200+1/20922789888e3*r))))))-.5)}(t),Math.exp(e)*Math.sin(t))},log:function(){var e=this.re,t=this.im;return new f(c(e,t),Math.atan2(t,e))},abs:function(){return function(e,t){var r=Math.abs(e),n=Math.abs(t);return r<3e3&&n<3e3?Math.sqrt(r*r+n*n):(n=r<n?(r=n,e/t):t/e,r*Math.sqrt(1+n*n))}(this.re,this.im)},arg:function(){return Math.atan2(this.im,this.re)},sin:function(){var e=this.re,t=this.im;return new f(Math.sin(e)*n(t),Math.cos(e)*i(t))},cos:function(){var e=this.re,t=this.im;return new f(Math.cos(e)*n(t),-Math.sin(e)*i(t))},tan:function(){var e=2*this.re,t=2*this.im,r=Math.cos(e)+n(t);return new f(Math.sin(e)/r,i(t)/r)},cot:function(){var e=2*this.re,t=2*this.im,r=Math.cos(e)-n(t);return new f(-Math.sin(e)/r,i(t)/r)},sec:function(){var e=this.re,t=this.im,r=.5*n(2*t)+.5*Math.cos(2*e);return new f(Math.cos(e)*n(t)/r,Math.sin(e)*i(t)/r)},csc:function(){var e=this.re,t=this.im,r=.5*n(2*t)-.5*Math.cos(2*e);return new f(Math.sin(e)*n(t)/r,-Math.cos(e)*i(t)/r)},asin:function(){var e=this.re,t=this.im,r=new f(t*t-e*e+1,-2*e*t).sqrt(),n=new f(r.re-t,r.im+e).log();return new f(n.im,-n.re)},acos:function(){var e=this.re,t=this.im,r=new f(t*t-e*e+1,-2*e*t).sqrt(),n=new f(r.re-t,r.im+e).log();return new f(Math.PI/2-n.im,n.re)},atan:function(){var e=this.re,t=this.im;if(0===e){if(1===t)return new f(0,1/0);if(-1===t)return new f(0,-1/0)}var r=e*e+(1-t)*(1-t),n=new f((1-t*t-e*e)/r,-2*e/r).log();return new f(-.5*n.im,.5*n.re)},acot:function(){var e=this.re,t=this.im;if(0===t)return new f(Math.atan2(1,e),0);var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).atan():new f(0!==e?e/0:0,0!==t?-t/0:0).atan()},asec:function(){var e=this.re,t=this.im;if(0===e&&0===t)return new f(0,1/0);var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).acos():new f(0!==e?e/0:0,0!==t?-t/0:0).acos()},acsc:function(){var e=this.re,t=this.im;if(0===e&&0===t)return new f(Math.PI/2,1/0);var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).asin():new f(0!==e?e/0:0,0!==t?-t/0:0).asin()},sinh:function(){var e=this.re,t=this.im;return new f(i(e)*Math.cos(t),n(e)*Math.sin(t))},cosh:function(){var e=this.re,t=this.im;return new f(n(e)*Math.cos(t),i(e)*Math.sin(t))},tanh:function(){var e=2*this.re,t=2*this.im,r=n(e)+Math.cos(t);return new f(i(e)/r,Math.sin(t)/r)},coth:function(){var e=2*this.re,t=2*this.im,r=n(e)-Math.cos(t);return new f(i(e)/r,-Math.sin(t)/r)},csch:function(){var e=this.re,t=this.im,r=Math.cos(2*t)-n(2*e);return new f(-2*i(e)*Math.cos(t)/r,2*n(e)*Math.sin(t)/r)},sech:function(){var e=this.re,t=this.im,r=Math.cos(2*t)+n(2*e);return new f(2*n(e)*Math.cos(t)/r,-2*i(e)*Math.sin(t)/r)},asinh:function(){var e=this.im;this.im=-this.re,this.re=e;var t=this.asin();return this.re=-this.im,this.im=e,e=t.re,t.re=-t.im,t.im=e,t},acosh:function(){var e=this.acos();if(e.im<=0){var t=e.re;e.re=-e.im,e.im=t}else{t=e.im;e.im=-e.re,e.re=t}return e},atanh:function(){var e=this.re,t=this.im,r=1<e&&0===t,n=1-e,i=1+e,a=n*n+t*t,o=0!=a?new f((i*n-t*t)/a,(t*n+i*t)/a):new f(-1!==e?e/0:0,0!==t?t/0:0),s=o.re;return o.re=c(o.re,o.im)/2,o.im=Math.atan2(o.im,s)/2,r&&(o.im=-o.im),o},acoth:function(){var e=this.re,t=this.im;if(0===e&&0===t)return new f(0,Math.PI/2);var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).atanh():new f(0!==e?e/0:0,0!==t?-t/0:0).atanh()},acsch:function(){var e=this.re,t=this.im;if(0===t)return new f(0!==e?Math.log(e+Math.sqrt(e*e+1)):1/0,0);var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).asinh():new f(0!==e?e/0:0,0!==t?-t/0:0).asinh()},asech:function(){var e=this.re,t=this.im;if(this.isZero())return f.INFINITY;var r=e*e+t*t;return 0!=r?new f(e/r,-t/r).acosh():new f(0!==e?e/0:0,0!==t?-t/0:0).acosh()},inverse:function(){if(this.isZero())return f.INFINITY;if(this.isInfinite())return f.ZERO;var e=this.re,t=this.im,r=e*e+t*t;return new f(e/r,-t/r)},conjugate:function(){return new f(this.re,-this.im)},neg:function(){return new f(-this.re,-this.im)},ceil:function(e){return e=Math.pow(10,e||0),new f(Math.ceil(this.re*e)/e,Math.ceil(this.im*e)/e)},floor:function(e){return e=Math.pow(10,e||0),new f(Math.floor(this.re*e)/e,Math.floor(this.im*e)/e)},round:function(e){return e=Math.pow(10,e||0),new f(Math.round(this.re*e)/e,Math.round(this.im*e)/e)},equals:function(e,t){var r=new f(e,t);return Math.abs(r.re-this.re)<=f.EPSILON&&Math.abs(r.im-this.im)<=f.EPSILON},clone:function(){return new f(this.re,this.im)},toString:function(){var e=this.re,t=this.im,r="";return this.isNaN()?"NaN":this.isZero()?"0":this.isInfinite()?"Infinity":(0!==e&&(r+=e),0!==t&&(0!==e?r+=t<0?" - ":" + ":t<0&&(r+="-"),1!==(t=Math.abs(t))&&(r+=t),r+="i"),r||"0")},toVector:function(){return[this.re,this.im]},valueOf:function(){return 0===this.im?this.re:null},isNaN:function(){return isNaN(this.re)||isNaN(this.im)},isZero:function(){return!(0!==this.re&&-0!==this.re||0!==this.im&&-0!==this.im)},isFinite:function(){return isFinite(this.re)&&isFinite(this.im)},isInfinite:function(){return!(this.isNaN()||this.isFinite())}},f.ZERO=new f(0,0),f.ONE=new f(1,0),f.I=new f(0,1),f.PI=new f(Math.PI,0),f.E=new f(Math.E,0),f.INFINITY=new f(1/0,1/0),f.NAN=new f(NaN,NaN),f.EPSILON=1e-16,void 0===(o=function(){return f}.apply(t,[]))||(e.exports=o)}()},function(e,t,r){"use strict";function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");this.index=e,arguments.length<3?(this.min=0,this.max=t):(this.min=t,this.max=r),void 0!==this.min&&this.index<this.min?this.message="Index out of range ("+this.index+" < "+this.min+")":void 0!==this.max&&this.index>=this.max?this.message="Index out of range ("+this.index+" > "+(this.max-1)+")":this.message="Index out of range ("+this.index+")",this.stack=(new Error).stack}r.d(t,"a",function(){return n}),(n.prototype=new RangeError).constructor=RangeError,n.prototype.name="IndexError",n.prototype.isIndexError=!0},function(r,i,e){var a;
+/**
+ * @license Fraction.js v4.0.12 09/09/2015
+ * http://www.xarg.org/2014/03/rational-numbers-in-javascript/
+ *
+ * Copyright (c) 2015, Robert Eisele (robert@xarg.org)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ **/
+/**
+ * @license Fraction.js v4.0.12 09/09/2015
+ * http://www.xarg.org/2014/03/rational-numbers-in-javascript/
+ *
+ * Copyright (c) 2015, Robert Eisele (robert@xarg.org)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ **/
+!function(){"use strict";var d={s:1,n:0,d:1};function e(t){function e(){var e=Error.apply(this,arguments);e.name=this.name=t,this.stack=e.stack,this.message=e.message}function r(){}return r.prototype=Error.prototype,e.prototype=new r,e}var y=c.DivisionByZero=e("DivisionByZero"),t=c.InvalidParameter=e("InvalidParameter");function g(e,t){return isNaN(e=parseInt(e,10))&&v(),e*t}function v(){throw new t}var n=function(e,t){var r,n=0,i=1,a=1,o=0,s=0,u=0,c=1,f=1,l=0,p=1,m=1,h=1;if(null==e);else if(void 0!==t)a=(n=e)*(i=t);else switch(typeof e){case"object":"d"in e&&"n"in e?(n=e.n,i=e.d,"s"in e&&(n*=e.s)):0 in e?(n=e[0],1 in e&&(i=e[1])):v(),a=n*i;break;case"number":if(e<0&&(e=-(a=e)),e%1==0)n=e;else if(0<e){for(1<=e&&(e/=f=Math.pow(10,Math.floor(1+Math.log(e)/Math.LN10)));p<=1e7&&h<=1e7;){if(e===(r=(l+m)/(p+h))){i=p+h<=1e7?(n=l+m,p+h):p<h?(n=m,h):(n=l,p);break}r<e?(l+=m,p+=h):(m+=l,h+=p),i=1e7<p?(n=m,h):(n=l,p)}n*=f}else(isNaN(e)||isNaN(t))&&(i=n=NaN);break;case"string":if(null===(p=e.match(/\d+|./g))&&v(),"-"===p[l]?(a=-1,l++):"+"===p[l]&&l++,p.length===l+1?s=g(p[l++],a):"."===p[l+1]||"."===p[l]?("."!==p[l]&&(o=g(p[l++],a)),(++l+1===p.length||"("===p[l+1]&&")"===p[l+3]||"'"===p[l+1]&&"'"===p[l+3])&&(s=g(p[l],a),c=Math.pow(10,p[l].length),l++),("("===p[l]&&")"===p[l+2]||"'"===p[l]&&"'"===p[l+2])&&(u=g(p[l+1],a),f=Math.pow(10,p[l+1].length)-1,l+=3)):"/"===p[l+1]||":"===p[l+1]?(s=g(p[l],a),c=g(p[l+2],1),l+=3):"/"===p[l+3]&&" "===p[l+1]&&(o=g(p[l],a),s=g(p[l+2],a),c=g(p[l+4],1),l+=5),p.length<=l){a=n=u+(i=c*f)*o+f*s;break}default:v()}if(0===i)throw new y;d.s=a<0?-1:1,d.n=Math.abs(n),d.d=Math.abs(i)};function u(e,t){if(!e)return t;if(!t)return e;for(;;){if(!(e%=t))return t;if(!(t%=e))return e}}function c(e,t){if(!(this instanceof c))return new c(e,t);n(e,t),e=c.REDUCE?u(d.d,d.n):1,this.s=d.s,this.n=d.n/e,this.d=d.d/e}c.REDUCE=1,c.prototype={s:1,n:0,d:1,abs:function(){return new c(this.n,this.d)},neg:function(){return new c(-this.s*this.n,this.d)},add:function(e,t){return n(e,t),new c(this.s*this.n*d.d+d.s*this.d*d.n,this.d*d.d)},sub:function(e,t){return n(e,t),new c(this.s*this.n*d.d-d.s*this.d*d.n,this.d*d.d)},mul:function(e,t){return n(e,t),new c(this.s*d.s*this.n*d.n,this.d*d.d)},div:function(e,t){return n(e,t),new c(this.s*d.s*this.n*d.d,this.d*d.n)},clone:function(){return new c(this)},mod:function(e,t){return isNaN(this.n)||isNaN(this.d)?new c(NaN):void 0===e?new c(this.s*this.n%this.d,1):(n(e,t),0===d.n&&0===this.d&&c(0,0),new c(this.s*(d.d*this.n)%(d.n*this.d),d.d*this.d))},gcd:function(e,t){return n(e,t),new c(u(d.n,this.n)*u(d.d,this.d),d.d*this.d)},lcm:function(e,t){return n(e,t),0===d.n&&0===this.n?new c:new c(d.n*this.n,u(d.n,this.n)*u(d.d,this.d))},ceil:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new c(NaN):new c(Math.ceil(e*this.s*this.n/this.d),e)},floor:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new c(NaN):new c(Math.floor(e*this.s*this.n/this.d),e)},round:function(e){return e=Math.pow(10,e||0),isNaN(this.n)||isNaN(this.d)?new c(NaN):new c(Math.round(e*this.s*this.n/this.d),e)},inverse:function(){return new c(this.s*this.d,this.n)},pow:function(e){return e<0?new c(Math.pow(this.s*this.d,-e),Math.pow(this.n,-e)):new c(Math.pow(this.s*this.n,e),Math.pow(this.d,e))},equals:function(e,t){return n(e,t),this.s*this.n*d.d==d.s*d.n*this.d},compare:function(e,t){n(e,t);var r=this.s*this.n*d.d-d.s*d.n*this.d;return(0<r)-(r<0)},simplify:function(e){if(isNaN(this.n)||isNaN(this.d))return this;var t=this.abs().toContinued();function r(e){return 1===e.length?new c(e[0]):r(e.slice(1)).inverse().add(e[0])}e=e||.001;for(var n=0;n<t.length;n++){var i=r(t.slice(0,n+1));if(i.sub(this.abs()).abs().valueOf()<e)return i.mul(this.s)}return this},divisible:function(e,t){return n(e,t),!(!(d.n*this.d)||this.n*d.d%(d.n*this.d))},valueOf:function(){return this.s*this.n/this.d},toFraction:function(e){var t,r="",n=this.n,i=this.d;return this.s<0&&(r+="-"),1===i?r+=n:(e&&0<(t=Math.floor(n/i))&&(r+=t,r+=" ",n%=i),r+=n,r+="/",r+=i),r},toLatex:function(e){var t,r="",n=this.n,i=this.d;return this.s<0&&(r+="-"),1===i?r+=n:(e&&0<(t=Math.floor(n/i))&&(r+=t,n%=i),r+="\\frac{",r+=n,r+="}{",r+=i,r+="}"),r},toContinued:function(){var e,t=this.n,r=this.d,n=[];if(isNaN(this.n)||isNaN(this.d))return n;for(;n.push(Math.floor(t/r)),e=t%r,t=r,r=e,1!==t;);return n},toString:function(e){var t,r=this.n,n=this.d;if(isNaN(r)||isNaN(n))return"NaN";c.REDUCE||(r/=t=u(r,n),n/=t),e=e||15;var i=function(e,t){for(;t%2==0;t/=2);for(;t%5==0;t/=5);if(1===t)return 0;for(var r=10%t,n=1;1!==r;n++)if(r=10*r%t,2e3<n)return 0;return n}(0,n),a=function(e,t,r){for(var n=1,i=function(e,t,r){for(var n=1;0<t;e=e*e%r,t>>=1)1&t&&(n=n*e%r);return n}(10,r,t),a=0;a<300;a++){if(n===i)return a;n=10*n%t,i=10*i%t}return 0}(0,n,i),o=-1===this.s?"-":"";if(o+=r/n|0,r%=n,(r*=10)&&(o+="."),i){for(var s=a;s--;)o+=r/n|0,r%=n,r*=10;o+="(";for(s=i;s--;)o+=r/n|0,r%=n,r*=10;o+=")"}else for(s=e;r&&s--;)o+=r/n|0,r%=n,r*=10;return o}},void 0===(a=function(){return c}.apply(i,[]))||(r.exports=a)}()},function(e,t){e.exports=function t(e,r){"use strict";function n(e){return t.insensitive&&(""+e).toLowerCase()||""+e}var i,a,o=/(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,s=/(^[ ]*|[ ]*$)/g,u=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,c=/^0x[0-9a-f]+$/i,f=/^0/,l=n(e).replace(s,"")||"",p=n(r).replace(s,"")||"",m=l.replace(o,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),h=p.replace(o,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),d=parseInt(l.match(c),16)||1!==m.length&&l.match(u)&&Date.parse(l),y=parseInt(p.match(c),16)||d&&p.match(u)&&Date.parse(p)||null;if(y){if(d<y)return-1;if(y<d)return 1}for(var g=0,v=Math.max(m.length,h.length);g<v;g++){if(i=!(m[g]||"").match(f)&&parseFloat(m[g])||m[g]||0,a=!(h[g]||"").match(f)&&parseFloat(h[g])||h[g]||0,isNaN(i)!==isNaN(a))return isNaN(i)?1:-1;if(typeof i!=typeof a&&(i+="",a+=""),i<a)return-1;if(a<i)return 1}return 0}},function(e,t,r){"use strict";function i(e,t,r,n){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");this.fn=e,this.count=t,this.min=r,this.max=n,this.message="Wrong number of arguments in function "+e+" ("+t+" provided, "+r+(null!=n?"-"+n:"")+" expected)",this.stack=(new Error).stack}r.d(t,"a",function(){return i}),(i.prototype=new Error).constructor=Error,i.prototype.name="ArgumentsError",i.prototype.isArgumentsError=!0},function(e,t,r){"use strict";var n,i,a;i=[],void 0===(a="function"==typeof(n=function(){function k(){return!0}function re(){return!1}function ne(){}return function e(){var t=[{name:"number",test:function(e){return"number"==typeof e}},{name:"string",test:function(e){return"string"==typeof e}},{name:"boolean",test:function(e){return"boolean"==typeof e}},{name:"Function",test:function(e){return"function"==typeof e}},{name:"Array",test:Array.isArray},{name:"Date",test:function(e){return e instanceof Date}},{name:"RegExp",test:function(e){return e instanceof RegExp}},{name:"Object",test:function(e){return"object"==typeof e&&null!==e&&e.constructor===Object}},{name:"null",test:function(e){return null===e}},{name:"undefined",test:function(e){return void 0===e}}];var n={name:"any",test:k};var r=[];var i=[];var U={types:t,conversions:i,ignore:r};function u(t){var e=ee(U.types,function(e){return e.name===t});if(e)return e;if("any"===t)return n;var r=ee(U.types,function(e){return e.name.toLowerCase()===t.toLowerCase()});throw new TypeError('Unknown type "'+t+'"'+(r?'. Did you mean "'+r.name+'"?':""))}function c(e){return e===n?999:U.types.indexOf(e)}function p(t){var e=ee(U.types,function(e){return e.test(t)});if(e)return e.name;throw new TypeError("Value has unknown type. Value: "+t)}function a(e,t){if(!e.signatures)throw new TypeError("Function is no typed-function");var r;if("string"==typeof t){r=t.split(",");for(var n=0;n<r.length;n++)r[n]=r[n].trim()}else{if(!Array.isArray(t))throw new TypeError("String array or a comma separated string expected");r=t}var i=r.join(","),a=e.signatures[i];if(a)return a;throw new TypeError("Signature not found (signature: "+(e.name||"unnamed")+"("+r.join(", ")+"))")}function o(e,t){var r=p(e);if(t===r)return e;for(var n=0;n<U.conversions.length;n++){var i=U.conversions[n];if(i.from===r&&i.to===t)return i.convert(e)}throw new Error("Cannot convert from "+r+" to "+t)}function L(e){return e.map(function(e){var t=e.types.map(h);return(e.restParam?"...":"")+t.join("|")}).join(",")}function s(e,r){var t=0===e.indexOf("..."),n=t?3<e.length?e.slice(3):"any":e,i=n.split("|").map(M).filter(E).filter(O),a=x(r,i),o=i.map(function(e){var t=u(e);return{name:e,typeIndex:c(t),test:t.test,conversion:null,conversionIndex:-1}}),s=a.map(function(e){var t=u(e.from);return{name:e.from,typeIndex:c(t),test:t.test,conversion:e,conversionIndex:r.indexOf(e)}});return{types:o.concat(s),restParam:t}}function H(e,t,i){var r=[];return""!==e.trim()&&(r=e.split(",").map(M).map(function(e,t,r){var n=s(e,i);if(n.restParam&&t!==r.length-1)throw new SyntaxError('Unexpected rest parameter "'+e+'": only allowed for the last parameter');return n})),r.some(j)?null:{params:r,fn:t}}function $(e){var t=A(e);return!!t&&t.restParam}function f(e){return e.types.some(function(e){return null!=e.conversion})}function G(e){if(e&&0!==e.types.length){if(1===e.types.length)return u(e.types[0].name).test;if(2===e.types.length){var t=u(e.types[0].name).test,r=u(e.types[1].name).test;return function(e){return t(e)||r(e)}}var n=e.types.map(function(e){return u(e.name).test});return function(e){for(var t=0;t<n.length;t++)if(n[t](e))return!0;return!1}}return k}function Z(e){var r,t,n;if($(e)){var i=(r=S(e).map(G)).length,a=G(A(e)),o=function(e){for(var t=i;t<e.length;t++)if(!a(e[t]))return!1;return!0};return function(e){for(var t=0;t<r.length;t++)if(!r[t](e[t]))return!1;return o(e)&&e.length>=i+1}}return 0===e.length?function(e){return 0===e.length}:1===e.length?(t=G(e[0]),function(e){return t(e[0])&&1===e.length}):2===e.length?(t=G(e[0]),n=G(e[1]),function(e){return t(e[0])&&n(e[1])&&2===e.length}):(r=e.map(G),function(e){for(var t=0;t<r.length;t++)if(!r[t](e[t]))return!1;return e.length===r.length})}function m(e,t){return t<e.params.length?e.params[t]:$(e.params)?A(e.params):null}function l(e,t,r){var n=m(e,t),i=n?r?n.types.filter(d):n.types:[];return i.map(h)}function h(e){return e.name}function d(e){return null===e.conversion||void 0===e.conversion}function y(e,t){var r=I(te(e,function(e){return l(e,t,!1)}));return-1!==r.indexOf("any")?["any"]:r}function V(e,r,t){var n,i,a,o=e||"unnamed",s=t;for(a=0;a<r.length;a++){var u=s.filter(function(e){var t=G(m(e,a));return(a<e.params.length||$(e.params))&&t(r[a])});if(0===u.length){if(0<(i=y(s,a)).length){var c=p(r[a]);return(n=new TypeError("Unexpected type of argument in function "+o+" (expected: "+i.join(" or ")+", actual: "+c+", index: "+a+")")).data={category:"wrongType",fn:o,index:a,actual:c,expected:i},n}}else s=u}var f=s.map(function(e){return $(e.params)?1/0:e.params.length});if(r.length<Math.min.apply(null,f))return i=y(s,a),(n=new TypeError("Too few arguments in function "+o+" (expected: "+i.join(" or ")+", index: "+r.length+")")).data={category:"tooFewArgs",fn:o,index:r.length,expected:i},n;var l=Math.max.apply(null,f);return r.length>l?(n=new TypeError("Too many arguments in function "+o+" (expected: "+l+", actual: "+r.length+")")).data={category:"tooManyArgs",fn:o,index:r.length,expectedLength:l}:(n=new TypeError('Arguments of type "'+r.join(", ")+'" do not match any of the defined signatures of function '+o+".")).data={category:"mismatch",actual:r.map(p)},n}function g(e){for(var t=999,r=0;r<e.types.length;r++)d(e.types[r])&&(t=Math.min(t,e.types[r].typeIndex));return t}function v(e){for(var t=999,r=0;r<e.types.length;r++)d(e.types[r])||(t=Math.min(t,e.types[r].conversionIndex));return t}function b(e,t){var r;return 0!==(r=e.restParam-t.restParam)?r:0!==(r=f(e)-f(t))?r:0!==(r=g(e)-g(t))?r:v(e)-v(t)}function J(e,t){var r,n,i=Math.min(e.params.length,t.params.length);if(0!==(n=e.params.some(f)-t.params.some(f)))return n;for(r=0;r<i;r++)if(0!==(n=f(e.params[r])-f(t.params[r])))return n;for(r=0;r<i;r++)if(0!==(n=b(e.params[r],t.params[r])))return n;return e.params.length-t.params.length}function x(e,t){var r={};return e.forEach(function(e){-1!==t.indexOf(e.from)||-1===t.indexOf(e.to)||r[e.from]||(r[e.from]=e)}),Object.keys(r).map(function(e){return r[e]})}function W(e,n){var t=n;if(e.some(f)){var i=$(e),a=e.map(w);t=function(){for(var e=[],t=i?arguments.length-1:arguments.length,r=0;r<t;r++)e[r]=a[r](arguments[r]);return i&&(e[t]=arguments[t].map(a[t])),n.apply(null,e)}}var r=t;if($(e)){var o=e.length-1;r=function(){return t.apply(null,C(arguments,0,o).concat([C(arguments,o)]))}}return r}function w(e){var t,r,n,i,a=[],o=[];switch(e.types.forEach(function(e){e.conversion&&(a.push(u(e.conversion.from).test),o.push(e.conversion.convert))}),o.length){case 0:return function(e){return e};case 1:return t=a[0],n=o[0],function(e){return t(e)?n(e):e};case 2:return t=a[0],r=a[1],n=o[0],i=o[1],function(e){return t(e)?n(e):r(e)?i(e):e};default:return function(e){for(var t=0;t<o.length;t++)if(a[t](e))return o[t](e);return e}}}function Y(e){var r={};return e.forEach(function(t){t.params.some(f)||X(t.params,!0).forEach(function(e){r[L(e)]=t.fn})}),r}function X(e,u){function c(r,t,n){if(t<r.length){var e,i=r[t],a=u?i.types.filter(d):i.types;if(i.restParam){var o=a.filter(d);e=o.length<a.length?[o,a]:[a]}else e=a.map(function(e){return[e]});return te(e,function(e){return c(r,t+1,n.concat([e]))})}var s=n.map(function(e,t){return{types:e,restParam:t===r.length-1&&$(r)}});return[s]}return c(e,0,[])}function Q(e,t){for(var r=Math.max(e.params.length,t.params.length),n=0;n<r;n++){var i=l(e,n,!0),a=l(t,n,!0);if(!_(i,a))return!1}var o=e.params.length,s=t.params.length,u=$(e.params),c=$(t.params);return u?c?o===s:o<=s:c?s<=o:o===s}function N(t,r){if(0===Object.keys(r).length)throw new SyntaxError("No signatures provided");var n=[];Object.keys(r).map(function(e){return H(e,r[e],U.conversions)}).filter(K).forEach(function(t){var e=ee(n,function(e){return Q(e,t)});if(e)throw new TypeError('Conflicting signatures "'+L(e.params)+'" and "'+L(t.params)+'".');n.push(t)});var i=te(n,function(t){var e=t?X(t.params,!1):[];return e.map(function(e){return{params:e,fn:t.fn}})}).filter(K);i.sort(J);var e=i[0]&&i[0].params.length<=2&&!$(i[0].params),a=i[1]&&i[1].params.length<=2&&!$(i[1].params),o=i[2]&&i[2].params.length<=2&&!$(i[2].params),s=i[3]&&i[3].params.length<=2&&!$(i[3].params),u=i[4]&&i[4].params.length<=2&&!$(i[4].params),c=i[5]&&i[5].params.length<=2&&!$(i[5].params),f=e&&a&&o&&s&&u&&c,l=i.map(function(e){return Z(e.params)}),p=e?G(i[0].params[0]):re,m=a?G(i[1].params[0]):re,h=o?G(i[2].params[0]):re,d=s?G(i[3].params[0]):re,y=u?G(i[4].params[0]):re,g=c?G(i[5].params[0]):re,v=e?G(i[0].params[1]):re,b=a?G(i[1].params[1]):re,x=o?G(i[2].params[1]):re,w=s?G(i[3].params[1]):re,N=u?G(i[4].params[1]):re,O=c?G(i[5].params[1]):re,M=i.map(function(e){return W(e.params,e.fn)}),E=e?M[0]:ne,j=a?M[1]:ne,S=o?M[2]:ne,A=s?M[3]:ne,C=u?M[4]:ne,T=c?M[5]:ne,_=e?i[0].params.length:-1,I=a?i[1].params.length:-1,q=o?i[2].params.length:-1,B=s?i[3].params.length:-1,k=u?i[4].params.length:-1,z=c?i[5].params.length:-1,D=f?6:0,R=i.length,P=function(){for(var e=D;e<R;e++)if(l[e](arguments))return M[e].apply(null,arguments);throw V(t,arguments,i)},F=function(e,t){return arguments.length===_&&p(e)&&v(t)?E.apply(null,arguments):arguments.length===I&&m(e)&&b(t)?j.apply(null,arguments):arguments.length===q&&h(e)&&x(t)?S.apply(null,arguments):arguments.length===B&&d(e)&&w(t)?A.apply(null,arguments):arguments.length===k&&y(e)&&N(t)?C.apply(null,arguments):arguments.length===z&&g(e)&&O(t)?T.apply(null,arguments):P.apply(null,arguments)};try{Object.defineProperty(F,"name",{value:t})}catch(e){}return F.signatures=Y(i),F}function O(e){return-1===U.ignore.indexOf(e)}function M(e){return e.trim()}function E(e){return!!e}function K(e){return null!==e}function j(e){return 0===e.types.length}function S(e){return e.slice(0,e.length-1)}function A(e){return e[e.length-1]}function C(e,t,r){return Array.prototype.slice.call(e,t,r)}function T(e,t){return-1!==e.indexOf(t)}function _(e,t){for(var r=0;r<e.length;r++)if(T(t,e[r]))return!0;return!1}function ee(e,t){for(var r=0;r<e.length;r++)if(t(e[r]))return e[r]}function I(e){for(var t={},r=0;r<e.length;r++)t[e[r]]=!0;return Object.keys(t)}function te(e,t){return Array.prototype.concat.apply([],e.map(t))}function q(e){for(var t="",r=0;r<e.length;r++){var n=e[r];if(("object"==typeof n.signatures||"string"==typeof n.signature)&&""!==n.name)if(""===t)t=n.name;else if(t!==n.name){var i=new Error("Function names do not match (expected: "+t+", actual: "+n.name+")");throw i.data={actual:n.name,expected:t},i}}return t}function B(e){var r,n={};function t(e,t){if(n.hasOwnProperty(e)&&t!==n[e])throw(r=new Error('Signature "'+e+'" is defined twice')).data={signature:e},r}for(var i=0;i<e.length;i++){var a=e[i];if("object"==typeof a.signatures)for(var o in a.signatures)a.signatures.hasOwnProperty(o)&&(t(o,a.signatures[o]),n[o]=a.signatures[o]);else{if("string"!=typeof a.signature)throw(r=new TypeError("Function is no typed-function (index: "+i+")")).data={index:i},r;t(a.signature,a),n[a.signature]=a}}return n}U=N("typed",{"string, Object":N,Object:function(e){var t=[];for(var r in e)e.hasOwnProperty(r)&&t.push(e[r]);var n=q(t);return N(n,e)},"...Function":function(e){return N(q(e),B(e))},"string, ...Function":function(e,t){return N(e,B(t))}});U.create=e;U.types=t;U.conversions=i;U.ignore=r;U.convert=o;U.find=a;U.addType=function(e,t){if(!e||"string"!=typeof e.name||"function"!=typeof e.test)throw new TypeError("Object with properties {name: string, test: function} expected");if(!1!==t)for(var r=0;r<U.types.length;r++)if("Object"===U.types[r].name)return void U.types.splice(r,0,e);U.types.push(e)};U.addConversion=function(e){if(!e||"string"!=typeof e.from||"string"!=typeof e.to||"function"!=typeof e.convert)throw new TypeError("Object with properties {from: string, to: string, convert: function} expected");U.conversions.push(e)};return U}()})?n.apply(t,i):n)||(e.exports=a)},function(m,e,t){"use strict";(function(e){var s=256,i=[],a=void 0===e?window:e,o=Math.pow(s,6),u=Math.pow(2,52),c=2*u,t=Math.random;function f(e){var t,r=e.length,o=this,n=0,i=o.i=o.j=0,a=o.S=[];for(r||(e=[r++]);n<s;)a[n]=n++;for(n=0;n<s;n++)a[n]=a[i=255&i+e[n%r]+(t=a[n])],a[i]=t;(o.g=function(e){for(var t,r=0,n=o.i,i=o.j,a=o.S;e--;)t=a[n=255&n+1],r=r*s+a[255&(a[n]=a[i=255&i+t])+(a[i]=t)];return o.i=n,o.j=i,r})(s)}function l(e,t){for(var r,n=e+"",i=0;i<n.length;)t[255&i]=255&(r^=19*t[255&i])+n.charCodeAt(i++);return p(t)}function p(e){return String.fromCharCode.apply(0,e)}m.exports=function(e,t){if(t&&!0===t.global)return t.global=!1,Math.random=m.exports(e,t),t.global=!0,Math.random;var r=[],n=(l(function e(t,r){var n,i=[],a=(typeof t)[0];if(r&&"o"==a)for(n in t)try{i.push(e(t[n],r-1))}catch(e){}return i.length?i:"s"==a?t:t+"\0"}(t&&t.entropy||!1?[e,p(i)]:0 in arguments?e:function(e){try{return a.crypto.getRandomValues(e=new Uint8Array(s)),p(e)}catch(e){return[+new Date,a,a.navigator&&a.navigator.plugins,a.screen,p(i)]}}(),3),r),new f(r));return l(p(n.S),i),function(){for(var e=n.g(6),t=o,r=0;e<u;)e=(e+r)*s,t*=s,r=n.g(1);for(;c<=e;)e/=2,t/=2,r>>>=1;return(e+r)/t}},m.exports.resetGlobal=function(){Math.random=t},l(Math.random(),i)}).call(this,t(20))},function(t,De,Re){var Pe;!function(){"use strict";var l,T,o,s=9e15,d=1e9,y="0123456789abcdef",n="2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058",i="3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789",u={precision:20,rounding:4,modulo:1,toExpNeg:-7,toExpPos:21,minE:-s,maxE:s,crypto:!1},b=!0,c="[DecimalError] ",g=c+"Invalid argument: ",a=c+"Precision limit exceeded",f=c+"crypto unavailable",_=Math.floor,v=Math.pow,p=/^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,m=/^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,h=/^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,x=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,I=1e7,q=7,w=n.length-1,N=i.length-1,O={name:"[object Decimal]"};function M(e){var t,r,n,i=e.length-1,a="",o=e[0];if(0<i){for(a+=o,t=1;t<i;t++)n=e[t]+"",(r=q-n.length)&&(a+=L(r)),a+=n;o=e[t],(r=q-(n=o+"").length)&&(a+=L(r))}else if(0===o)return"0";for(;o%10==0;)o/=10;return a+o}function E(e,t,r){if(e!==~~e||e<t||r<e)throw Error(g+e)}function j(e,t,r,n){var i,a,o;for(a=e[0];10<=a;a/=10)--t;return--t<0?(t+=q,i=0):(i=Math.ceil((t+1)/q),t%=q),a=v(10,q-t),o=e[i]%a|0,null==n?t<3?(0==t?o=o/100|0:1==t&&(o=o/10|0),r<4&&99999==o||3<r&&49999==o||5e4==o||0==o):(r<4&&o+1==a||3<r&&o+1==a/2)&&(e[i+1]/a/100|0)==v(10,t-2)-1||(o==a/2||0==o)&&0==(e[i+1]/a/100|0):t<4?(0==t?o=o/1e3|0:1==t?o=o/100|0:2==t&&(o=o/10|0),(n||r<4)&&9999==o||!n&&3<r&&4999==o):((n||r<4)&&o+1==a||!n&&3<r&&o+1==a/2)&&(e[i+1]/a/1e3|0)==v(10,t-3)-1}function S(e,t,r){for(var n,i,a=[0],o=0,s=e.length;o<s;){for(i=a.length;i--;)a[i]*=t;for(a[0]+=y.indexOf(e.charAt(o++)),n=0;n<a.length;n++)a[n]>r-1&&(void 0===a[n+1]&&(a[n+1]=0),a[n+1]+=a[n]/r|0,a[n]%=r)}return a.reverse()}O.absoluteValue=O.abs=function(){var e=new this.constructor(this);return e.s<0&&(e.s=1),D(e)},O.ceil=function(){return D(new this.constructor(this),this.e+1,2)},O.comparedTo=O.cmp=function(e){var t,r,n,i,a=this,o=a.d,s=(e=new a.constructor(e)).d,u=a.s,c=e.s;if(!o||!s)return u&&c?u!==c?u:o===s?0:!o^u<0?1:-1:NaN;if(!o[0]||!s[0])return o[0]?u:s[0]?-c:0;if(u!==c)return u;if(a.e!==e.e)return a.e>e.e^u<0?1:-1;for(t=0,r=(n=o.length)<(i=s.length)?n:i;t<r;++t)if(o[t]!==s[t])return o[t]>s[t]^u<0?1:-1;return n===i?0:i<n^u<0?1:-1},O.cosine=O.cos=function(){var e,t,r=this,n=r.constructor;return r.d?r.d[0]?(e=n.precision,t=n.rounding,n.precision=e+Math.max(r.e,r.sd())+q,n.rounding=1,r=function(e,t){var r,n,i=t.d.length;n=i<32?(r=Math.ceil(i/3),(1/Y(4,r)).toString()):(r=16,"2.3283064365386962890625e-10");e.precision+=r,t=W(e,1,t.times(n),new e(1));for(var a=r;a--;){var o=t.times(t);t=o.times(o).minus(o).times(8).plus(1)}return e.precision-=r,t}(n,X(n,r)),n.precision=e,n.rounding=t,D(2==o||3==o?r.neg():r,e,t,!0)):new n(1):new n(NaN)},O.cubeRoot=O.cbrt=function(){var e,t,r,n,i,a,o,s,u,c,f=this,l=f.constructor;if(!f.isFinite()||f.isZero())return new l(f);for(b=!1,(a=f.s*v(f.s*f,1/3))&&Math.abs(a)!=1/0?n=new l(a.toString()):(r=M(f.d),(a=((e=f.e)-r.length+1)%3)&&(r+=1==a||-2==a?"0":"00"),a=v(r,1/3),e=_((e+1)/3)-(e%3==(e<0?-1:2)),(n=new l(r=a==1/0?"5e"+e:(r=a.toExponential()).slice(0,r.indexOf("e")+1)+e)).s=f.s),o=(e=l.precision)+3;;)if(c=(u=(s=n).times(s).times(s)).plus(f),n=A(c.plus(f).times(s),c.plus(u),o+2,1),M(s.d).slice(0,o)===(r=M(n.d)).slice(0,o)){if("9999"!=(r=r.slice(o-3,o+1))&&(i||"4999"!=r)){+r&&(+r.slice(1)||"5"!=r.charAt(0))||(D(n,e+1,1),t=!n.times(n).times(n).eq(f));break}if(!i&&(D(s,e+1,0),s.times(s).times(s).eq(f))){n=s;break}o+=4,i=1}return b=!0,D(n,e,l.rounding,t)},O.decimalPlaces=O.dp=function(){var e,t=this.d,r=NaN;if(t){if(r=((e=t.length-1)-_(this.e/q))*q,e=t[e])for(;e%10==0;e/=10)r--;r<0&&(r=0)}return r},O.dividedBy=O.div=function(e){return A(this,new this.constructor(e))},O.dividedToIntegerBy=O.divToInt=function(e){var t=this.constructor;return D(A(this,new t(e),0,1,1),t.precision,t.rounding)},O.equals=O.eq=function(e){return 0===this.cmp(e)},O.floor=function(){return D(new this.constructor(this),this.e+1,3)},O.greaterThan=O.gt=function(e){return 0<this.cmp(e)},O.greaterThanOrEqualTo=O.gte=function(e){var t=this.cmp(e);return 1==t||0===t},O.hyperbolicCosine=O.cosh=function(){var e,t,r,n,i,a=this,o=a.constructor,s=new o(1);if(!a.isFinite())return new o(a.s?1/0:NaN);if(a.isZero())return s;r=o.precision,n=o.rounding,o.precision=r+Math.max(a.e,a.sd())+4,o.rounding=1,t=(i=a.d.length)<32?(1/Y(4,e=Math.ceil(i/3))).toString():(e=16,"2.3283064365386962890625e-10"),a=W(o,1,a.times(t),new o(1),!0);for(var u,c=e,f=new o(8);c--;)u=a.times(a),a=s.minus(u.times(f.minus(u.times(f))));return D(a,o.precision=r,o.rounding=n,!0)},O.hyperbolicSine=O.sinh=function(){var e,t,r,n,i=this,a=i.constructor;if(!i.isFinite()||i.isZero())return new a(i);if(t=a.precision,r=a.rounding,a.precision=t+Math.max(i.e,i.sd())+4,a.rounding=1,(n=i.d.length)<3)i=W(a,2,i,i,!0);else{e=16<(e=1.4*Math.sqrt(n))?16:0|e,i=W(a,2,i=i.times(1/Y(5,e)),i,!0);for(var o,s=new a(5),u=new a(16),c=new a(20);e--;)o=i.times(i),i=i.times(s.plus(o.times(u.times(o).plus(c))))}return D(i,a.precision=t,a.rounding=r,!0)},O.hyperbolicTangent=O.tanh=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+7,n.rounding=1,A(r.sinh(),r.cosh(),n.precision=e,n.rounding=t)):new n(r.s)},O.inverseCosine=O.acos=function(){var e,t=this,r=t.constructor,n=t.abs().cmp(1),i=r.precision,a=r.rounding;return-1!==n?0===n?t.isNeg()?F(r,i,a):new r(0):new r(NaN):t.isZero()?F(r,i+4,a).times(.5):(r.precision=i+6,r.rounding=1,t=t.asin(),e=F(r,i+4,a).times(.5),r.precision=i,r.rounding=a,e.minus(t))},O.inverseHyperbolicCosine=O.acosh=function(){var e,t,r=this,n=r.constructor;return r.lte(1)?new n(r.eq(1)?0:NaN):r.isFinite()?(e=n.precision,t=n.rounding,n.precision=e+Math.max(Math.abs(r.e),r.sd())+4,n.rounding=1,b=!1,r=r.times(r).minus(1).sqrt().plus(r),b=!0,n.precision=e,n.rounding=t,r.ln()):new n(r)},O.inverseHyperbolicSine=O.asinh=function(){var e,t,r=this,n=r.constructor;return!r.isFinite()||r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+2*Math.max(Math.abs(r.e),r.sd())+6,n.rounding=1,b=!1,r=r.times(r).plus(1).sqrt().plus(r),b=!0,n.precision=e,n.rounding=t,r.ln())},O.inverseHyperbolicTangent=O.atanh=function(){var e,t,r,n,i=this,a=i.constructor;return i.isFinite()?0<=i.e?new a(i.abs().eq(1)?i.s/0:i.isZero()?i:NaN):(e=a.precision,t=a.rounding,n=i.sd(),Math.max(n,e)<2*-i.e-1?D(new a(i),e,t,!0):(a.precision=r=n-i.e,i=A(i.plus(1),new a(1).minus(i),r+e,1),a.precision=e+4,a.rounding=1,i=i.ln(),a.precision=e,a.rounding=t,i.times(.5))):new a(NaN)},O.inverseSine=O.asin=function(){var e,t,r,n,i=this,a=i.constructor;return i.isZero()?new a(i):(t=i.abs().cmp(1),r=a.precision,n=a.rounding,-1!==t?0===t?((e=F(a,r+4,n).times(.5)).s=i.s,e):new a(NaN):(a.precision=r+6,a.rounding=1,i=i.div(new a(1).minus(i.times(i)).sqrt().plus(1)).atan(),a.precision=r,a.rounding=n,i.times(2)))},O.inverseTangent=O.atan=function(){var e,t,r,n,i,a,o,s,u,c=this,f=c.constructor,l=f.precision,p=f.rounding;if(c.isFinite()){if(c.isZero())return new f(c);if(c.abs().eq(1)&&l+4<=N)return(o=F(f,l+4,p).times(.25)).s=c.s,o}else{if(!c.s)return new f(NaN);if(l+4<=N)return(o=F(f,l+4,p).times(.5)).s=c.s,o}for(f.precision=s=l+10,f.rounding=1,e=r=Math.min(28,s/q+2|0);e;--e)c=c.div(c.times(c).plus(1).sqrt().plus(1));for(b=!1,t=Math.ceil(s/q),n=1,u=c.times(c),o=new f(c),i=c;-1!==e;)if(i=i.times(u),a=o.minus(i.div(n+=2)),i=i.times(u),void 0!==(o=a.plus(i.div(n+=2))).d[t])for(e=t;o.d[e]===a.d[e]&&e--;);return r&&(o=o.times(2<<r-1)),b=!0,D(o,f.precision=l,f.rounding=p,!0)},O.isFinite=function(){return!!this.d},O.isInteger=O.isInt=function(){return!!this.d&&_(this.e/q)>this.d.length-2},O.isNaN=function(){return!this.s},O.isNegative=O.isNeg=function(){return this.s<0},O.isPositive=O.isPos=function(){return 0<this.s},O.isZero=function(){return!!this.d&&0===this.d[0]},O.lessThan=O.lt=function(e){return this.cmp(e)<0},O.lessThanOrEqualTo=O.lte=function(e){return this.cmp(e)<1},O.logarithm=O.log=function(e){var t,r,n,i,a,o,s,u,c=this,f=c.constructor,l=f.precision,p=f.rounding;if(null==e)e=new f(10),t=!0;else{if(r=(e=new f(e)).d,e.s<0||!r||!r[0]||e.eq(1))return new f(NaN);t=e.eq(10)}if(r=c.d,c.s<0||!r||!r[0]||c.eq(1))return new f(r&&!r[0]?-1/0:1!=c.s?NaN:r?0:1/0);if(t)if(1<r.length)a=!0;else{for(i=r[0];i%10==0;)i/=10;a=1!==i}if(b=!1,o=Z(c,s=l+5),n=t?P(f,s+10):Z(e,s),j((u=A(o,n,s,1)).d,i=l,p))do{if(o=Z(c,s+=10),n=t?P(f,s+10):Z(e,s),u=A(o,n,s,1),!a){+M(u.d).slice(i+1,i+15)+1==1e14&&(u=D(u,l+1,0));break}}while(j(u.d,i+=10,p));return b=!0,D(u,l,p)},O.minus=O.sub=function(e){var t,r,n,i,a,o,s,u,c,f,l,p,m=this,h=m.constructor;if(e=new h(e),!m.d||!e.d)return m.s&&e.s?m.d?e.s=-e.s:e=new h(e.d||m.s!==e.s?m:NaN):e=new h(NaN),e;if(m.s!=e.s)return e.s=-e.s,m.plus(e);if(c=m.d,p=e.d,s=h.precision,u=h.rounding,!c[0]||!p[0]){if(p[0])e.s=-e.s;else{if(!c[0])return new h(3===u?-0:0);e=new h(m)}return b?D(e,s,u):e}if(r=_(e.e/q),f=_(m.e/q),c=c.slice(),a=f-r){for(o=(l=a<0)?(t=c,a=-a,p.length):(t=p,r=f,c.length),(n=Math.max(Math.ceil(s/q),o)+2)<a&&(a=n,t.length=1),t.reverse(),n=a;n--;)t.push(0);t.reverse()}else{for((l=(n=c.length)<(o=p.length))&&(o=n),n=0;n<o;n++)if(c[n]!=p[n]){l=c[n]<p[n];break}a=0}for(l&&(t=c,c=p,p=t,e.s=-e.s),o=c.length,n=p.length-o;0<n;--n)c[o++]=0;for(n=p.length;a<n;){if(c[--n]<p[n]){for(i=n;i&&0===c[--i];)c[i]=I-1;--c[i],c[n]+=I}c[n]-=p[n]}for(;0===c[--o];)c.pop();for(;0===c[0];c.shift())--r;return c[0]?(e.d=c,e.e=R(c,r),b?D(e,s,u):e):new h(3===u?-0:0)},O.modulo=O.mod=function(e){var t,r=this,n=r.constructor;return e=new n(e),!r.d||!e.s||e.d&&!e.d[0]?new n(NaN):!e.d||r.d&&!r.d[0]?D(new n(r),n.precision,n.rounding):(b=!1,9==n.modulo?(t=A(r,e.abs(),0,3,1)).s*=e.s:t=A(r,e,0,n.modulo,1),t=t.times(e),b=!0,r.minus(t))},O.naturalExponential=O.exp=function(){return G(this)},O.naturalLogarithm=O.ln=function(){return Z(this)},O.negated=O.neg=function(){var e=new this.constructor(this);return e.s=-e.s,D(e)},O.plus=O.add=function(e){var t,r,n,i,a,o,s,u,c,f,l=this,p=l.constructor;if(e=new p(e),!l.d||!e.d)return l.s&&e.s?l.d||(e=new p(e.d||l.s===e.s?l:NaN)):e=new p(NaN),e;if(l.s!=e.s)return e.s=-e.s,l.minus(e);if(c=l.d,f=e.d,s=p.precision,u=p.rounding,!c[0]||!f[0])return f[0]||(e=new p(l)),b?D(e,s,u):e;if(a=_(l.e/q),n=_(e.e/q),c=c.slice(),i=a-n){for((o=(o=i<0?(r=c,i=-i,f.length):(r=f,n=a,c.length))<(a=Math.ceil(s/q))?a+1:o+1)<i&&(i=o,r.length=1),r.reverse();i--;)r.push(0);r.reverse()}for((o=c.length)-(i=f.length)<0&&(i=o,r=f,f=c,c=r),t=0;i;)t=(c[--i]=c[i]+f[i]+t)/I|0,c[i]%=I;for(t&&(c.unshift(t),++n),o=c.length;0==c[--o];)c.pop();return e.d=c,e.e=R(c,n),b?D(e,s,u):e},O.precision=O.sd=function(e){var t;if(void 0!==e&&e!==!!e&&1!==e&&0!==e)throw Error(g+e);return this.d?(t=U(this.d),e&&this.e+1>t&&(t=this.e+1)):t=NaN,t},O.round=function(){var e=this.constructor;return D(new e(this),this.e+1,e.rounding)},O.sine=O.sin=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+Math.max(r.e,r.sd())+q,n.rounding=1,r=function(e,t){var r,n=t.d.length;if(n<3)return W(e,2,t,t);r=16<(r=1.4*Math.sqrt(n))?16:0|r,t=t.times(1/Y(5,r)),t=W(e,2,t,t);for(var i,a=new e(5),o=new e(16),s=new e(20);r--;)i=t.times(t),t=t.times(a.plus(i.times(o.times(i).minus(s))));return t}(n,X(n,r)),n.precision=e,n.rounding=t,D(2<o?r.neg():r,e,t,!0)):new n(NaN)},O.squareRoot=O.sqrt=function(){var e,t,r,n,i,a,o=this,s=o.d,u=o.e,c=o.s,f=o.constructor;if(1!==c||!s||!s[0])return new f(!c||c<0&&(!s||s[0])?NaN:s?o:1/0);for(b=!1,n=0==(c=Math.sqrt(+o))||c==1/0?(((t=M(s)).length+u)%2==0&&(t+="0"),c=Math.sqrt(t),u=_((u+1)/2)-(u<0||u%2),new f(t=c==1/0?"1e"+u:(t=c.toExponential()).slice(0,t.indexOf("e")+1)+u)):new f(c.toString()),r=(u=f.precision)+3;;)if(n=(a=n).plus(A(o,a,r+2,1)).times(.5),M(a.d).slice(0,r)===(t=M(n.d)).slice(0,r)){if("9999"!=(t=t.slice(r-3,r+1))&&(i||"4999"!=t)){+t&&(+t.slice(1)||"5"!=t.charAt(0))||(D(n,u+1,1),e=!n.times(n).eq(o));break}if(!i&&(D(a,u+1,0),a.times(a).eq(o))){n=a;break}r+=4,i=1}return b=!0,D(n,u,f.rounding,e)},O.tangent=O.tan=function(){var e,t,r=this,n=r.constructor;return r.isFinite()?r.isZero()?new n(r):(e=n.precision,t=n.rounding,n.precision=e+10,n.rounding=1,(r=r.sin()).s=1,r=A(r,new n(1).minus(r.times(r)).sqrt(),e+10,0),n.precision=e,n.rounding=t,D(2==o||4==o?r.neg():r,e,t,!0)):new n(NaN)},O.times=O.mul=function(e){var t,r,n,i,a,o,s,u,c,f=this.constructor,l=this.d,p=(e=new f(e)).d;if(e.s*=this.s,!(l&&l[0]&&p&&p[0]))return new f(!e.s||l&&!l[0]&&!p||p&&!p[0]&&!l?NaN:l&&p?0*e.s:e.s/0);for(r=_(this.e/q)+_(e.e/q),(u=l.length)<(c=p.length)&&(a=l,l=p,p=a,o=u,u=c,c=o),a=[],n=o=u+c;n--;)a.push(0);for(n=c;0<=--n;){for(t=0,i=u+n;n<i;)s=a[i]+p[n]*l[i-n-1]+t,a[i--]=s%I|0,t=s/I|0;a[i]=(a[i]+t)%I|0}for(;!a[--o];)a.pop();return t?++r:a.shift(),e.d=a,e.e=R(a,r),b?D(e,f.precision,f.rounding):e},O.toBinary=function(e,t){return r(this,2,e,t)},O.toDecimalPlaces=O.toDP=function(e,t){var r=this,n=r.constructor;return r=new n(r),void 0===e?r:(E(e,0,d),void 0===t?t=n.rounding:E(t,0,8),D(r,e+r.e+1,t))},O.toExponential=function(e,t){var r,n=this,i=n.constructor;return r=void 0===e?C(n,!0):(E(e,0,d),void 0===t?t=i.rounding:E(t,0,8),C(n=D(new i(n),e+1,t),!0,e+1)),n.isNeg()&&!n.isZero()?"-"+r:r},O.toFixed=function(e,t){var r,n,i=this,a=i.constructor;return r=void 0===e?C(i):(E(e,0,d),void 0===t?t=a.rounding:E(t,0,8),C(n=D(new a(i),e+i.e+1,t),!1,e+n.e+1)),i.isNeg()&&!i.isZero()?"-"+r:r},O.toFraction=function(e){var t,r,n,i,a,o,s,u,c,f,l,p,m=this,h=m.d,d=m.constructor;if(!h)return new d(m);if(c=r=new d(1),o=(a=(t=new d(n=u=new d(0))).e=U(h)-m.e-1)%q,t.d[0]=v(10,o<0?q+o:o),null==e)e=0<a?t:c;else{if(!(s=new d(e)).isInt()||s.lt(c))throw Error(g+s);e=s.gt(t)?0<a?t:c:s}for(b=!1,s=new d(M(h)),f=d.precision,d.precision=a=h.length*q*2;l=A(s,t,0,1,1),1!=(i=r.plus(l.times(n))).cmp(e);)r=n,n=i,i=c,c=u.plus(l.times(i)),u=i,i=t,t=s.minus(l.times(i)),s=i;return i=A(e.minus(r),n,0,1,1),u=u.plus(i.times(c)),r=r.plus(i.times(n)),u.s=c.s=m.s,p=A(c,n,a,1).minus(m).abs().cmp(A(u,r,a,1).minus(m).abs())<1?[c,n]:[u,r],d.precision=f,b=!0,p},O.toHexadecimal=O.toHex=function(e,t){return r(this,16,e,t)},O.toNearest=function(e,t){var r=this,n=r.constructor;if(r=new n(r),null==e){if(!r.d)return r;e=new n(1),t=n.rounding}else{if(e=new n(e),void 0===t?t=n.rounding:E(t,0,8),!r.d)return e.s?r:e;if(!e.d)return e.s&&(e.s=r.s),e}return e.d[0]?(b=!1,r=A(r,e,0,t,1).times(e),b=!0,D(r)):(e.s=r.s,r=e),r},O.toNumber=function(){return+this},O.toOctal=function(e,t){return r(this,8,e,t)},O.toPower=O.pow=function(e){var t,r,n,i,a,o,s=this,u=s.constructor,c=+(e=new u(e));if(!(s.d&&e.d&&s.d[0]&&e.d[0]))return new u(v(+s,c));if((s=new u(s)).eq(1))return s;if(n=u.precision,a=u.rounding,e.eq(1))return D(s,n,a);if((t=_(e.e/q))>=e.d.length-1&&(r=c<0?-c:c)<=9007199254740991)return i=H(u,s,r,n),e.s<0?new u(1).div(i):D(i,n,a);if((o=s.s)<0){if(t<e.d.length-1)return new u(NaN);if(0==(1&e.d[t])&&(o=1),0==s.e&&1==s.d[0]&&1==s.d.length)return s.s=o,s}return(t=0!=(r=v(+s,c))&&isFinite(r)?new u(r+"").e:_(c*(Math.log("0."+M(s.d))/Math.LN10+s.e+1)))>u.maxE+1||t<u.minE-1?new u(0<t?o/0:0):(b=!1,u.rounding=s.s=1,r=Math.min(12,(t+"").length),(i=G(e.times(Z(s,n+r)),n)).d&&j((i=D(i,n+5,1)).d,n,a)&&(t=n+10,+M((i=D(G(e.times(Z(s,t+r)),t),t+5,1)).d).slice(n+1,n+15)+1==1e14&&(i=D(i,n+1,0))),i.s=o,b=!0,D(i,n,u.rounding=a))},O.toPrecision=function(e,t){var r,n=this,i=n.constructor;return r=void 0===e?C(n,n.e<=i.toExpNeg||n.e>=i.toExpPos):(E(e,1,d),void 0===t?t=i.rounding:E(t,0,8),C(n=D(new i(n),e,t),e<=n.e||n.e<=i.toExpNeg,e)),n.isNeg()&&!n.isZero()?"-"+r:r},O.toSignificantDigits=O.toSD=function(e,t){var r=this.constructor;return void 0===e?(e=r.precision,t=r.rounding):(E(e,1,d),void 0===t?t=r.rounding:E(t,0,8)),D(new r(this),e,t)},O.toString=function(){var e=this,t=e.constructor,r=C(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()&&!e.isZero()?"-"+r:r},O.truncated=O.trunc=function(){return D(new this.constructor(this),this.e+1,1)},O.valueOf=O.toJSON=function(){var e=this,t=e.constructor,r=C(e,e.e<=t.toExpNeg||e.e>=t.toExpPos);return e.isNeg()?"-"+r:r};var A=function(e,t,r,n,i,a){var o,s,u,c,f,l,p,m,h,d,y,g,v,b,x,w,N,O,M,E,j=e.constructor,S=e.s==t.s?1:-1,A=e.d,C=t.d;if(!(A&&A[0]&&C&&C[0]))return new j(e.s&&t.s&&(A?!C||A[0]!=C[0]:C)?A&&0==A[0]||!C?0*S:S/0:NaN);for(s=a?(f=1,e.e-t.e):(a=I,f=q,_(e.e/f)-_(t.e/f)),M=C.length,N=A.length,d=(h=new j(S)).d=[],u=0;C[u]==(A[u]||0);u++);if(C[u]>(A[u]||0)&&s--,null==r?(b=r=j.precision,n=j.rounding):b=i?r+(e.e-t.e)+1:r,b<0)d.push(1),l=!0;else{if(b=b/f+2|0,u=0,1==M){for(C=C[c=0],b++;(u<N||c)&&b--;u++)x=c*a+(A[u]||0),d[u]=x/C|0,c=x%C|0;l=c||u<N}else{for(1<(c=a/(C[0]+1)|0)&&(C=B(C,c,a),A=B(A,c,a),M=C.length,N=A.length),w=M,g=(y=A.slice(0,M)).length;g<M;)y[g++]=0;for((E=C.slice()).unshift(0),O=C[0],C[1]>=a/2&&++O;c=0,(o=k(C,y,M,g))<0?(v=y[0],M!=g&&(v=v*a+(y[1]||0)),1<(c=v/O|0)?(a<=c&&(c=a-1),1==(o=k(p=B(C,c,a),y,m=p.length,g=y.length))&&(c--,z(p,M<m?E:C,m,a))):(0==c&&(o=c=1),p=C.slice()),(m=p.length)<g&&p.unshift(0),z(y,p,g,a),-1==o&&(o=k(C,y,M,g=y.length))<1&&(c++,z(y,M<g?E:C,g,a)),g=y.length):0===o&&(c++,y=[0]),d[u++]=c,o&&y[0]?y[g++]=A[w]||0:(y=[A[w]],g=1),(w++<N||void 0!==y[0])&&b--;);l=void 0!==y[0]}d[0]||d.shift()}if(1==f)h.e=s,T=l;else{for(u=1,c=d[0];10<=c;c/=10)u++;h.e=u+s*f-1,D(h,i?r+h.e+1:r,n,l)}return h};function B(e,t,r){var n,i=0,a=e.length;for(e=e.slice();a--;)n=e[a]*t+i,e[a]=n%r|0,i=n/r|0;return i&&e.unshift(i),e}function k(e,t,r,n){var i,a;if(r!=n)a=n<r?1:-1;else for(i=a=0;i<r;i++)if(e[i]!=t[i]){a=e[i]>t[i]?1:-1;break}return a}function z(e,t,r,n){for(var i=0;r--;)e[r]-=i,i=e[r]<t[r]?1:0,e[r]=i*n+e[r]-t[r];for(;!e[0]&&1<e.length;)e.shift()}function D(e,t,r,n){var i,a,o,s,u,c,f,l,p,m=e.constructor;e:if(null!=t){if(!(l=e.d))return e;for(i=1,s=l[0];10<=s;s/=10)i++;if((a=t-i)<0)a+=q,o=t,u=(f=l[p=0])/v(10,i-o-1)%10|0;else if(p=Math.ceil((a+1)/q),(s=l.length)<=p){if(!n)break e;for(;s++<=p;)l.push(0);f=u=0,o=(a%=q)-q+(i=1)}else{for(f=s=l[p],i=1;10<=s;s/=10)i++;u=(o=(a%=q)-q+i)<0?0:f/v(10,i-o-1)%10|0}if(n=n||t<0||void 0!==l[p+1]||(o<0?f:f%v(10,i-o-1)),c=r<4?(u||n)&&(0==r||r==(e.s<0?3:2)):5<u||5==u&&(4==r||n||6==r&&(0<a?0<o?f/v(10,i-o):0:l[p-1])%10&1||r==(e.s<0?8:7)),t<1||!l[0])return l.length=0,c?(t-=e.e+1,l[0]=v(10,(q-t%q)%q),e.e=-t||0):l[0]=e.e=0,e;if(0==a?(l.length=p,s=1,p--):(l.length=p+1,s=v(10,q-a),l[p]=0<o?(f/v(10,i-o)%v(10,o)|0)*s:0),c)for(;;){if(0==p){for(a=1,o=l[0];10<=o;o/=10)a++;for(o=l[0]+=s,s=1;10<=o;o/=10)s++;a!=s&&(e.e++,l[0]==I&&(l[0]=1));break}if(l[p]+=s,l[p]!=I)break;l[p--]=0,s=1}for(a=l.length;0===l[--a];)l.pop()}return b&&(e.e>m.maxE?(e.d=null,e.e=NaN):e.e<m.minE&&(e.e=0,e.d=[0])),e}function C(e,t,r){if(!e.isFinite())return V(e);var n,i=e.e,a=M(e.d),o=a.length;return t?(r&&0<(n=r-o)?a=a.charAt(0)+"."+a.slice(1)+L(n):1<o&&(a=a.charAt(0)+"."+a.slice(1)),a=a+(e.e<0?"e":"e+")+e.e):i<0?(a="0."+L(-i-1)+a,r&&0<(n=r-o)&&(a+=L(n))):o<=i?(a+=L(i+1-o),r&&0<(n=r-i-1)&&(a=a+"."+L(n))):((n=i+1)<o&&(a=a.slice(0,n)+"."+a.slice(n)),r&&0<(n=r-o)&&(i+1===o&&(a+="."),a+=L(n))),a}function R(e,t){var r=e[0];for(t*=q;10<=r;r/=10)t++;return t}function P(e,t,r){if(w<t)throw b=!0,r&&(e.precision=r),Error(a);return D(new e(n),t,1,!0)}function F(e,t,r){if(N<t)throw Error(a);return D(new e(i),t,r,!0)}function U(e){var t=e.length-1,r=t*q+1;if(t=e[t]){for(;t%10==0;t/=10)r--;for(t=e[0];10<=t;t/=10)r++}return r}function L(e){for(var t="";e--;)t+="0";return t}function H(e,t,r,n){var i,a=new e(1),o=Math.ceil(n/q+4);for(b=!1;;){if(r%2&&Q((a=a.times(t)).d,o)&&(i=!0),0===(r=_(r/2))){r=a.d.length-1,i&&0===a.d[r]&&++a.d[r];break}Q((t=t.times(t)).d,o)}return b=!0,a}function $(e){return 1&e.d[e.d.length-1]}function e(e,t,r){for(var n,i=new e(t[0]),a=0;++a<t.length;){if(!(n=new e(t[a])).s){i=n;break}i[r](n)&&(i=n)}return i}function G(e,t){var r,n,i,a,o,s,u,c=0,f=0,l=0,p=e.constructor,m=p.rounding,h=p.precision;if(!e.d||!e.d[0]||17<e.e)return new p(e.d?e.d[0]?e.s<0?0:1/0:1:e.s?e.s<0?0:e:NaN);for(u=null==t?(b=!1,h):t,s=new p(.03125);-2<e.e;)e=e.times(s),l+=5;for(u+=n=Math.log(v(2,l))/Math.LN10*2+5|0,r=a=o=new p(1),p.precision=u;;){if(a=D(a.times(e),u,1),r=r.times(++f),M((s=o.plus(A(a,r,u,1))).d).slice(0,u)===M(o.d).slice(0,u)){for(i=l;i--;)o=D(o.times(o),u,1);if(null!=t)return p.precision=h,o;if(!(c<3&&j(o.d,u-n,m,c)))return D(o,p.precision=h,m,b=!0);p.precision=u+=10,r=a=s=new p(1),f=0,c++}o=s}}function Z(e,t){var r,n,i,a,o,s,u,c,f,l,p,m=1,h=e,d=h.d,y=h.constructor,g=y.rounding,v=y.precision;if(h.s<0||!d||!d[0]||!h.e&&1==d[0]&&1==d.length)return new y(d&&!d[0]?-1/0:1!=h.s?NaN:d?0:h);if(f=null==t?(b=!1,v):t,y.precision=f+=10,n=(r=M(d)).charAt(0),!(Math.abs(a=h.e)<15e14))return c=P(y,f+2,v).times(a+""),h=Z(new y(n+"."+r.slice(1)),f-10).plus(c),y.precision=v,null==t?D(h,v,g,b=!0):h;for(;n<7&&1!=n||1==n&&3<r.charAt(1);)n=(r=M((h=h.times(e)).d)).charAt(0),m++;for(a=h.e,1<n?(h=new y("0."+r),a++):h=new y(n+"."+r.slice(1)),u=o=h=A((l=h).minus(1),h.plus(1),f,1),p=D(h.times(h),f,1),i=3;;){if(o=D(o.times(p),f,1),M((c=u.plus(A(o,new y(i),f,1))).d).slice(0,f)===M(u.d).slice(0,f)){if(u=u.times(2),0!==a&&(u=u.plus(P(y,f+2,v).times(a+""))),u=A(u,new y(m),f,1),null!=t)return y.precision=v,u;if(!j(u.d,f-10,g,s))return D(u,y.precision=v,g,b=!0);y.precision=f+=10,c=o=h=A(l.minus(1),l.plus(1),f,1),p=D(h.times(h),f,1),i=s=1}u=c,i+=2}}function V(e){return String(e.s*e.s/0)}function J(e,t){var r,n,i;for(-1<(r=t.indexOf("."))&&(t=t.replace(".","")),0<(n=t.search(/e/i))?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;48===t.charCodeAt(n);n++);for(i=t.length;48===t.charCodeAt(i-1);--i);if(t=t.slice(n,i)){if(i-=n,e.e=r=r-n-1,e.d=[],n=(r+1)%q,r<0&&(n+=q),n<i){for(n&&e.d.push(+t.slice(0,n)),i-=q;n<i;)e.d.push(+t.slice(n,n+=q));t=t.slice(n),n=q-t.length}else n-=i;for(;n--;)t+="0";e.d.push(+t),b&&(e.e>e.constructor.maxE?(e.d=null,e.e=NaN):e.e<e.constructor.minE&&(e.e=0,e.d=[0]))}else e.e=0,e.d=[0];return e}function W(e,t,r,n,i){var a,o,s,u,c=e.precision,f=Math.ceil(c/q);for(b=!1,u=r.times(r),s=new e(n);;){if(o=A(s.times(u),new e(t++*t++),c,1),s=i?n.plus(o):n.minus(o),n=A(o.times(u),new e(t++*t++),c,1),void 0!==(o=s.plus(n)).d[f]){for(a=f;o.d[a]===s.d[a]&&a--;);if(-1==a)break}a=s,s=n,n=o,o=a,0}return b=!0,o.d.length=f+1,o}function Y(e,t){for(var r=e;--t;)r*=e;return r}function X(e,t){var r,n=t.s<0,i=F(e,e.precision,1),a=i.times(.5);if((t=t.abs()).lte(a))return o=n?4:1,t;if((r=t.divToInt(i)).isZero())o=n?3:2;else{if((t=t.minus(r.times(i))).lte(a))return o=$(r)?n?2:3:n?4:1,t;o=$(r)?n?1:4:n?3:2}return t.minus(i).abs()}function r(e,t,r,n){var i,a,o,s,u,c,f,l,p,m=e.constructor,h=void 0!==r;if(h?(E(r,1,d),void 0===n?n=m.rounding:E(n,0,8)):(r=m.precision,n=m.rounding),e.isFinite()){for(h?(i=2,16==t?r=4*r-3:8==t&&(r=3*r-2)):i=t,0<=(o=(f=C(e)).indexOf("."))&&(f=f.replace(".",""),(p=new m(1)).e=f.length-o,p.d=S(C(p),10,i),p.e=p.d.length),a=u=(l=S(f,10,i)).length;0==l[--u];)l.pop();if(l[0]){if(o<0?a--:((e=new m(e)).d=l,e.e=a,l=(e=A(e,p,r,n,0,i)).d,a=e.e,c=T),o=l[r],s=i/2,c=c||void 0!==l[r+1],c=n<4?(void 0!==o||c)&&(0===n||n===(e.s<0?3:2)):s<o||o===s&&(4===n||c||6===n&&1&l[r-1]||n===(e.s<0?8:7)),l.length=r,c)for(;++l[--r]>i-1;)l[r]=0,r||(++a,l.unshift(1));for(u=l.length;!l[u-1];--u);for(o=0,f="";o<u;o++)f+=y.charAt(l[o]);if(h){if(1<u)if(16==t||8==t){for(o=16==t?4:3,--u;u%o;u++)f+="0";for(u=(l=S(f,i,t)).length;!l[u-1];--u);for(o=1,f="1.";o<u;o++)f+=y.charAt(l[o])}else f=f.charAt(0)+"."+f.slice(1);f=f+(a<0?"p":"p+")+a}else if(a<0){for(;++a;)f="0"+f;f="0."+f}else if(++a>u)for(a-=u;a--;)f+="0";else a<u&&(f=f.slice(0,a)+"."+f.slice(a))}else f=h?"0p+0":"0";f=(16==t?"0x":2==t?"0b":8==t?"0o":"")+f}else f=V(e);return e.s<0?"-"+f:f}function Q(e,t){if(e.length>t)return e.length=t,!0}function K(e){return new this(e).abs()}function ee(e){return new this(e).acos()}function te(e){return new this(e).acosh()}function re(e,t){return new this(e).plus(t)}function ne(e){return new this(e).asin()}function ie(e){return new this(e).asinh()}function ae(e){return new this(e).atan()}function oe(e){return new this(e).atanh()}function se(e,t){e=new this(e),t=new this(t);var r,n=this.precision,i=this.rounding,a=n+4;return e.s&&t.s?e.d||t.d?!t.d||e.isZero()?(r=t.s<0?F(this,n,i):new this(0)).s=e.s:!e.d||t.isZero()?(r=F(this,a,1).times(.5)).s=e.s:r=t.s<0?(this.precision=a,this.rounding=1,r=this.atan(A(e,t,a,1)),t=F(this,a,1),this.precision=n,this.rounding=i,e.s<0?r.minus(t):r.plus(t)):this.atan(A(e,t,a,1)):(r=F(this,a,1).times(0<t.s?.25:.75)).s=e.s:r=new this(NaN),r}function ue(e){return new this(e).cbrt()}function ce(e){return D(e=new this(e),e.e+1,2)}function fe(e){if(!e||"object"!=typeof e)throw Error(c+"Object expected");var t,r,n,i=!0===e.defaults,a=["precision",1,d,"rounding",0,8,"toExpNeg",-s,0,"toExpPos",0,s,"maxE",0,s,"minE",-s,0,"modulo",0,9];for(t=0;t<a.length;t+=3)if(r=a[t],i&&(this[r]=u[r]),void 0!==(n=e[r])){if(!(_(n)===n&&a[t+1]<=n&&n<=a[t+2]))throw Error(g+r+": "+n);this[r]=n}if(r="crypto",i&&(this[r]=u[r]),void 0!==(n=e[r])){if(!0!==n&&!1!==n&&0!==n&&1!==n)throw Error(g+r+": "+n);if(n){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw Error(f);this[r]=!0}else this[r]=!1}return this}function le(e){return new this(e).cos()}function pe(e){return new this(e).cosh()}function me(e,t){return new this(e).div(t)}function he(e){return new this(e).exp()}function de(e){return D(e=new this(e),e.e+1,3)}function ye(){var e,t,r=new this(0);for(b=!1,e=0;e<arguments.length;)if((t=new this(arguments[e++])).d)r.d&&(r=r.plus(t.times(t)));else{if(t.s)return b=!0,new this(1/0);r=t}return b=!0,r.sqrt()}function ge(e){return e instanceof l||e&&"[object Decimal]"===e.name||!1}function ve(e){return new this(e).ln()}function be(e,t){return new this(e).log(t)}function xe(e){return new this(e).log(2)}function we(e){return new this(e).log(10)}function Ne(){return e(this,arguments,"lt")}function Oe(){return e(this,arguments,"gt")}function Me(e,t){return new this(e).mod(t)}function Ee(e,t){return new this(e).mul(t)}function je(e,t){return new this(e).pow(t)}function Se(e){var t,r,n,i,a=0,o=new this(1),s=[];if(void 0===e?e=this.precision:E(e,1,d),n=Math.ceil(e/q),this.crypto)if(crypto.getRandomValues)for(t=crypto.getRandomValues(new Uint32Array(n));a<n;)429e7<=(i=t[a])?t[a]=crypto.getRandomValues(new Uint32Array(1))[0]:s[a++]=i%1e7;else{if(!crypto.randomBytes)throw Error(f);for(t=crypto.randomBytes(n*=4);a<n;)214e7<=(i=t[a]+(t[a+1]<<8)+(t[a+2]<<16)+((127&t[a+3])<<24))?crypto.randomBytes(4).copy(t,a):(s.push(i%1e7),a+=4);a=n/4}else for(;a<n;)s[a++]=1e7*Math.random()|0;for(n=s[--a],e%=q,n&&e&&(i=v(10,q-e),s[a]=(n/i|0)*i);0===s[a];a--)s.pop();if(a<0)s=[r=0];else{for(r=-1;0===s[0];r-=q)s.shift();for(n=1,i=s[0];10<=i;i/=10)n++;n<q&&(r-=q-n)}return o.e=r,o.d=s,o}function Ae(e){return D(e=new this(e),e.e+1,this.rounding)}function Ce(e){return(e=new this(e)).d?e.d[0]?e.s:0*e.s:e.s||NaN}function Te(e){return new this(e).sin()}function _e(e){return new this(e).sinh()}function Ie(e){return new this(e).sqrt()}function qe(e,t){return new this(e).sub(t)}function Be(e){return new this(e).tan()}function ke(e){return new this(e).tanh()}function ze(e){return D(e=new this(e),e.e+1,1)}(l=function e(t){var r,n,i;function a(e){var t,r,n,i=this;if(!(i instanceof a))return new a(e);if(e instanceof(i.constructor=a))return i.s=e.s,void(b?!e.d||e.e>a.maxE?(i.e=NaN,i.d=null):e.e<a.minE?(i.e=0,i.d=[0]):(i.e=e.e,i.d=e.d.slice()):(i.e=e.e,i.d=e.d?e.d.slice():e.d));if("number"==(n=typeof e)){if(0===e)return i.s=1/e<0?-1:1,i.e=0,void(i.d=[0]);if(i.s=e<0?(e=-e,-1):1,e===~~e&&e<1e7){for(t=0,r=e;10<=r;r/=10)t++;return void(b?a.maxE<t?(i.e=NaN,i.d=null):t<a.minE?(i.e=0,i.d=[0]):(i.e=t,i.d=[e]):(i.e=t,i.d=[e]))}return 0*e!=0?(e||(i.s=NaN),i.e=NaN,void(i.d=null)):J(i,e.toString())}if("string"!=n)throw Error(g+e);return 45===(r=e.charCodeAt(0))?(e=e.slice(1),i.s=-1):(43===r&&(e=e.slice(1)),i.s=1),x.test(e)?J(i,e):function(e,t){var r,n,i,a,o,s,u,c,f;if("Infinity"===t||"NaN"===t)return+t||(e.s=NaN),e.e=NaN,e.d=null,e;if(m.test(t))r=16,t=t.toLowerCase();else if(p.test(t))r=2;else{if(!h.test(t))throw Error(g+t);r=8}for(o=0<=(a=(t=0<(a=t.search(/p/i))?(u=+t.slice(a+1),t.substring(2,a)):t.slice(2)).indexOf(".")),n=e.constructor,o&&(a=(s=(t=t.replace(".","")).length)-a,i=H(n,new n(r),a,2*a)),a=f=(c=S(t,r,I)).length-1;0===c[a];--a)c.pop();return a<0?new n(0*e.s):(e.e=R(c,f),e.d=c,b=!1,o&&(e=A(e,i,4*s)),u&&(e=e.times(Math.abs(u)<54?v(2,u):l.pow(2,u))),b=!0,e)}(i,e)}if(a.prototype=O,a.ROUND_UP=0,a.ROUND_DOWN=1,a.ROUND_CEIL=2,a.ROUND_FLOOR=3,a.ROUND_HALF_UP=4,a.ROUND_HALF_DOWN=5,a.ROUND_HALF_EVEN=6,a.ROUND_HALF_CEIL=7,a.ROUND_HALF_FLOOR=8,a.EUCLID=9,a.config=a.set=fe,a.clone=e,a.isDecimal=ge,a.abs=K,a.acos=ee,a.acosh=te,a.add=re,a.asin=ne,a.asinh=ie,a.atan=ae,a.atanh=oe,a.atan2=se,a.cbrt=ue,a.ceil=ce,a.cos=le,a.cosh=pe,a.div=me,a.exp=he,a.floor=de,a.hypot=ye,a.ln=ve,a.log=be,a.log10=we,a.log2=xe,a.max=Ne,a.min=Oe,a.mod=Me,a.mul=Ee,a.pow=je,a.random=Se,a.round=Ae,a.sign=Ce,a.sin=Te,a.sinh=_e,a.sqrt=Ie,a.sub=qe,a.tan=Be,a.tanh=ke,a.trunc=ze,void 0===t&&(t={}),t&&!0!==t.defaults)for(i=["precision","rounding","toExpNeg","toExpPos","maxE","minE","modulo","crypto"],r=0;r<i.length;)t.hasOwnProperty(n=i[r++])||(t[n]=this[n]);return a.config(t),a}(u)).default=l.Decimal=l,n=new l(n),i=new l(i),void 0===(Pe=function(){return l}.call(De,Re,De,t))||(t.exports=Pe)}()},function(e,t,r){"use strict";function l(e,t){return p({},e,t)}var p=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e},m={"{":"\\{","}":"\\}","\\":"\\textbackslash{}","#":"\\#",$:"\\$","%":"\\%","&":"\\&","^":"\\textasciicircum{}",_:"\\_","~":"\\textasciitilde{}"},h={"–":"\\--","—":"\\---"," ":"~","\t":"\\qquad{}","\r\n":"\\newline{}","\n":"\\newline{}"};e.exports=function(e){for(var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=t.preserveFormatting,n=void 0!==r&&r,i=t.escapeMapFn,a=void 0===i?l:i,o=String(e),s="",u=a(p({},m),n?p({},h):{}),c=Object.keys(u),f=function(){var r=!1;c.forEach(function(e,t){r||o.length>=e.length&&o.slice(0,e.length)===e&&(s+=u[c[t]],o=o.slice(e.length,o.length),r=!0)}),r||(s+=o.slice(0,1),o=o.slice(1,o.length))};o;)f();return s}},function(e,t){function r(){}r.prototype={on:function(e,t,r){var n=this.e||(this.e={});return(n[e]||(n[e]=[])).push({fn:t,ctx:r}),this},once:function(e,t,r){var n=this;function i(){n.off(e,i),t.apply(r,arguments)}return i._=t,this.on(e,i,r)},emit:function(e){for(var t=[].slice.call(arguments,1),r=((this.e||(this.e={}))[e]||[]).slice(),n=0,i=r.length;n<i;n++)r[n].fn.apply(r[n].ctx,t);return this},off:function(e,t){var r=this.e||(this.e={}),n=r[e],i=[];if(n&&t)for(var a=0,o=n.length;a<o;a++)n[a].fn!==t&&n[a].fn._!==t&&i.push(n[a]);return i.length?r[e]=i:delete r[e],this}},e.exports=r,e.exports.TinyEmitter=r},function(e,t,r){var n=r(21),i=(0,r(22).create)(n);e.exports=i},function(e,t){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(e){"object"==typeof window&&(r=window)}e.exports=r},function(e,t,r){"use strict";r.r(t);var ie=r(1),n=r(14),i=r.n(n),j=r(4),s=r(0),o=function(){return o=i.a.create,i.a},a=["?BigNumber","?Complex","?DenseMatrix","?Fraction"],u=Object(s.a)("typed",a,function(e){var r=e.BigNumber,n=e.Complex,t=e.DenseMatrix,i=e.Fraction,a=o();return a.types=[{name:"number",test:ie.y},{name:"Complex",test:ie.j},{name:"BigNumber",test:ie.e},{name:"Fraction",test:ie.o},{name:"Unit",test:ie.L},{name:"string",test:ie.I},{name:"Chain",test:ie.h},{name:"Array",test:ie.b},{name:"Matrix",test:ie.v},{name:"DenseMatrix",test:ie.n},{name:"SparseMatrix",test:ie.H},{name:"Range",test:ie.D},{name:"Index",test:ie.t},{name:"boolean",test:ie.g},{name:"ResultSet",test:ie.G},{name:"Help",test:ie.s},{name:"function",test:ie.p},{name:"Date",test:ie.m},{name:"RegExp",test:ie.F},{name:"null",test:ie.x},{name:"undefined",test:ie.K},{name:"AccessorNode",test:ie.a},{name:"ArrayNode",test:ie.c},{name:"AssignmentNode",test:ie.d},{name:"BlockNode",test:ie.f},{name:"ConditionalNode",test:ie.k},{name:"ConstantNode",test:ie.l},{name:"FunctionNode",test:ie.r},{name:"FunctionAssignmentNode",test:ie.q},{name:"IndexNode",test:ie.u},{name:"Node",test:ie.w},{name:"ObjectNode",test:ie.A},{name:"OperatorNode",test:ie.B},{name:"ParenthesisNode",test:ie.C},{name:"RangeNode",test:ie.E},{name:"SymbolNode",test:ie.J},{name:"Object",test:ie.z}],a.conversions=[{from:"number",to:"BigNumber",convert:function(e){if(r||c(e),15<Object(j.f)(e))throw new TypeError("Cannot implicitly convert a number with >15 significant digits to BigNumber (value: "+e+"). Use function bignumber(x) to convert to BigNumber.");return new r(e)}},{from:"number",to:"Complex",convert:function(e){return n||f(e),new n(e,0)}},{from:"number",to:"string",convert:function(e){return e+""}},{from:"BigNumber",to:"Complex",convert:function(e){return n||f(e),new n(e.toNumber(),0)}},{from:"Fraction",to:"BigNumber",convert:function(e){throw new TypeError("Cannot implicitly convert a Fraction to BigNumber or vice versa. Use function bignumber(x) to convert to BigNumber or fraction(x) to convert to Fraction.")}},{from:"Fraction",to:"Complex",convert:function(e){return n||f(e),new n(e.valueOf(),0)}},{from:"number",to:"Fraction",convert:function(e){i||l(e);var t=new i(e);if(t.valueOf()!==e)throw new TypeError("Cannot implicitly convert a number to a Fraction when there will be a loss of precision (value: "+e+"). Use function fraction(x) to convert to Fraction.");return t}},{from:"string",to:"number",convert:function(e){var t=Number(e);if(isNaN(t))throw new Error('Cannot convert "'+e+'" to a number');return t}},{from:"string",to:"BigNumber",convert:function(t){r||c(t);try{return new r(t)}catch(e){throw new Error('Cannot convert "'+t+'" to BigNumber')}}},{from:"string",to:"Fraction",convert:function(t){i||l(t);try{return new i(t)}catch(e){throw new Error('Cannot convert "'+t+'" to Fraction')}}},{from:"string",to:"Complex",convert:function(t){n||f(t);try{return new n(t)}catch(e){throw new Error('Cannot convert "'+t+'" to Complex')}}},{from:"boolean",to:"number",convert:function(e){return+e}},{from:"boolean",to:"BigNumber",convert:function(e){return r||c(e),new r(+e)}},{from:"boolean",to:"Fraction",convert:function(e){return i||l(e),new i(+e)}},{from:"boolean",to:"string",convert:function(e){return String(e)}},{from:"Array",to:"Matrix",convert:function(e){return t||function(){throw new Error("Cannot convert array into a Matrix: no class 'DenseMatrix' provided")}(),new t(e)}},{from:"Matrix",to:"Array",convert:function(e){return e.valueOf()}}],a});function c(e){throw new Error("Cannot convert value ".concat(e," into a BigNumber: no class 'BigNumber' provided"))}function f(e){throw new Error("Cannot convert value ".concat(e," into a Complex number: no class 'Complex' provided"))}function l(e){throw new Error("Cannot convert value ".concat(e," into a Fraction, no class 'Fraction' provided."))}var p=[],m=Object(s.a)("ResultSet",p,function(){function t(e){if(!(this instanceof t))throw new SyntaxError("Constructor must be called with the new operator");this.entries=e||[]}return t.prototype.type="ResultSet",t.prototype.isResultSet=!0,t.prototype.valueOf=function(){return this.entries},t.prototype.toString=function(){return"["+this.entries.join(", ")+"]"},t.prototype.toJSON=function(){return{mathjs:"ResultSet",entries:this.entries}},t.fromJSON=function(e){return new t(e.entries)},t},{isClass:!0}),h=r(16),d=r.n(h),y=["?on","config"],g=Object(s.a)("BigNumber",y,function(e){var t=e.on,r=e.config,n=d.a.clone({precision:r.precision});return n.prototype.type="BigNumber",n.prototype.isBigNumber=!0,n.prototype.toJSON=function(){return{mathjs:"BigNumber",value:this.toString()}},n.fromJSON=function(e){return new n(e.value)},t&&t("config",function(e,t){e.precision!==t.precision&&n.config({precision:e.precision})}),n},{isClass:!0}),v=r(9),b=r.n(v);function x(e){return(x="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var w=[],N=Object(s.a)("Complex",w,function(){return b.a.prototype.type="Complex",b.a.prototype.isComplex=!0,b.a.prototype.toJSON=function(){return{mathjs:"Complex",re:this.re,im:this.im}},b.a.prototype.toPolar=function(){return{r:this.abs(),phi:this.arg()}},b.a.prototype.format=function(e){var t=this.im,r=this.re,n=Object(j.h)(this.re,e),i=Object(j.h)(this.im,e),a=Object(ie.y)(e)?e:e?e.precision:null;if(null!==a){var o=Math.pow(10,-a);Math.abs(r/t)<o&&(r=0),Math.abs(t/r)<o&&(t=0)}return 0===t?n:0===r?1===t?"i":-1===t?"-i":i+"i":t<0?-1===t?n+" - i":n+" - "+i.substring(1)+"i":1===t?n+" + i":n+" + "+i+"i"},b.a.fromPolar=function(e){switch(arguments.length){case 1:var t=e;if("object"===x(t))return b()(t);throw new TypeError("Input has to be an object with r and phi keys.");case 2:var r=e,n=arguments[1];if(Object(ie.y)(r)){if(Object(ie.L)(n)&&n.hasBase("ANGLE")&&(n=n.toNumber("rad")),Object(ie.y)(n))return new b.a({r:r,phi:n});throw new TypeError("Phi is not a number nor an angle unit.")}throw new TypeError("Radius r is not a number.");default:throw new SyntaxError("Wrong number of arguments in function fromPolar")}},b.a.prototype.valueOf=b.a.prototype.toString,b.a.fromJSON=function(e){return new b.a(e)},b.a.compare=function(e,t){return e.re>t.re?1:e.re<t.re?-1:e.im>t.im?1:e.im<t.im?-1:0},b.a},{isClass:!0}),O=r(11),M=r.n(O),E=[],S=Object(s.a)("Fraction",E,function(){return M.a.prototype.type="Fraction",M.a.prototype.isFraction=!0,M.a.prototype.toJSON=function(){return{mathjs:"Fraction",n:this.s*this.n,d:this.d}},M.a.fromJSON=function(e){return new M.a(e)},M.a},{isClass:!0}),A=[],C=Object(s.a)("Range",A,function(){function o(e,t,r){if(!(this instanceof o))throw new SyntaxError("Constructor must be called with the new operator");var n=null!=e,i=null!=t,a=null!=r;if(n)if(Object(ie.e)(e))e=e.toNumber();else if("number"!=typeof e)throw new TypeError("Parameter start must be a number");if(i)if(Object(ie.e)(t))t=t.toNumber();else if("number"!=typeof t)throw new TypeError("Parameter end must be a number");if(a)if(Object(ie.e)(r))r=r.toNumber();else if("number"!=typeof r)throw new TypeError("Parameter step must be a number");this.start=n?parseFloat(e):0,this.end=i?parseFloat(t):0,this.step=a?parseFloat(r):1}return o.prototype.type="Range",o.prototype.isRange=!0,o.parse=function(e){if("string"!=typeof e)return null;var t=e.split(":").map(function(e){return parseFloat(e)});if(t.some(function(e){return isNaN(e)}))return null;switch(t.length){case 2:return new o(t[0],t[1]);case 3:return new o(t[0],t[2],t[1]);default:return null}},o.prototype.clone=function(){return new o(this.start,this.end,this.step)},o.prototype.size=function(){var e=0,t=this.start,r=this.step,n=this.end-t;return Object(j.n)(r)===Object(j.n)(n)?e=Math.ceil(n/r):0==n&&(e=0),isNaN(e)&&(e=0),[e]},o.prototype.min=function(){var e=this.size()[0];return 0<e?0<this.step?this.start:this.start+(e-1)*this.step:void 0},o.prototype.max=function(){var e=this.size()[0];return 0<e?0<this.step?this.start+(e-1)*this.step:this.start:void 0},o.prototype.forEach=function(e){var t=this.start,r=this.step,n=this.end,i=0;if(0<r)for(;t<n;)e(t,[i],this),t+=r,i++;else if(r<0)for(;n<t;)e(t,[i],this),t+=r,i++},o.prototype.map=function(n){var i=[];return this.forEach(function(e,t,r){i[t[0]]=n(e,t,r)}),i},o.prototype.toArray=function(){var r=[];return this.forEach(function(e,t){r[t[0]]=e}),r},o.prototype.valueOf=function(){return this.toArray()},o.prototype.format=function(e){var t=Object(j.h)(this.start,e);return 1!==this.step&&(t+=":"+Object(j.h)(this.step,e)),t+=":"+Object(j.h)(this.end,e)},o.prototype.toString=function(){return this.format()},o.prototype.toJSON=function(){return{mathjs:"Range",start:this.start,end:this.end,step:this.step}},o.fromJSON=function(e){return new o(e.start,e.end,e.step)},o},{isClass:!0}),T=[],_=Object(s.a)("Matrix",T,function(){function e(){if(!(this instanceof e))throw new SyntaxError("Constructor must be called with the new operator")}return e.prototype.type="Matrix",e.prototype.isMatrix=!0,e.storage=function(e){throw new Error("Matrix.storage is deprecated since v6.0.0. Use the factory function math.matrix instead.")},e.prototype.storage=function(){throw new Error("Cannot invoke storage on a Matrix interface")},e.prototype.datatype=function(){throw new Error("Cannot invoke datatype on a Matrix interface")},e.prototype.create=function(e,t){throw new Error("Cannot invoke create on a Matrix interface")},e.prototype.subset=function(e,t,r){throw new Error("Cannot invoke subset on a Matrix interface")},e.prototype.get=function(e){throw new Error("Cannot invoke get on a Matrix interface")},e.prototype.set=function(e,t,r){throw new Error("Cannot invoke set on a Matrix interface")},e.prototype.resize=function(e,t){throw new Error("Cannot invoke resize on a Matrix interface")},e.prototype.reshape=function(e,t){throw new Error("Cannot invoke reshape on a Matrix interface")},e.prototype.clone=function(){throw new Error("Cannot invoke clone on a Matrix interface")},e.prototype.size=function(){throw new Error("Cannot invoke size on a Matrix interface")},e.prototype.map=function(e,t){throw new Error("Cannot invoke map on a Matrix interface")},e.prototype.forEach=function(e){throw new Error("Cannot invoke forEach on a Matrix interface")},e.prototype.toArray=function(){throw new Error("Cannot invoke toArray on a Matrix interface")},e.prototype.valueOf=function(){throw new Error("Cannot invoke valueOf on a Matrix interface")},e.prototype.format=function(e){throw new Error("Cannot invoke format on a Matrix interface")},e.prototype.toString=function(){throw new Error("Cannot invoke toString on a Matrix interface")},e},{isClass:!0}),I=r(2),J=r(5),ae=r(3),D=r(6),q=["Matrix"],B=Object(s.a)("DenseMatrix",q,function(e){var t=e.Matrix;function m(e,t){if(!(this instanceof m))throw new SyntaxError("Constructor must be called with the new operator");if(t&&!Object(ie.I)(t))throw new Error("Invalid datatype: "+t);if(Object(ie.v)(e))"DenseMatrix"===e.type?(this._data=Object(ae.a)(e._data),this._size=Object(ae.a)(e._size)):(this._data=e.toArray(),this._size=e.size()),this._datatype=t||e._datatype;else if(e&&Object(ie.b)(e.data)&&Object(ie.b)(e.size))this._data=e.data,this._size=e.size,this._datatype=t||e.datatype;else if(Object(ie.b)(e))this._data=function e(t){for(var r=0,n=t.length;r<n;r++){var i=t[r];Object(ie.b)(i)?t[r]=e(i):i&&!0===i.isMatrix&&(t[r]=e(i.valueOf()))}return t}(e),this._size=Object(I.a)(this._data),Object(I.r)(this._data,this._size),this._datatype=t;else{if(e)throw new TypeError("Unsupported type of data ("+Object(ie.M)(e)+")");this._data=[],this._size=[0],this._datatype=t}}function s(e,t,r){if(0!==t.length)return e._size=t.slice(0),e._data=Object(I.o)(e._data,e._size,r),e;for(var n=e._data;Object(ie.b)(n);)n=n[0];return n}function l(e,t,r){for(var n=e._size.slice(0),i=!1;n.length<t.length;)n.push(0),i=!0;for(var a=0,o=t.length;a<o;a++)t[a]>n[a]&&(n[a]=t[a],i=!0);i&&s(e,n,r)}return(m.prototype=new t).createDenseMatrix=function(e,t){return new m(e,t)},m.prototype.type="DenseMatrix",m.prototype.isDenseMatrix=!0,m.prototype.getDataType=function(){return Object(I.h)(this._data,ie.M)},m.prototype.storage=function(){return"dense"},m.prototype.datatype=function(){return this._datatype},m.prototype.create=function(e,t){return new m(e,t)},m.prototype.subset=function(e,t,r){switch(arguments.length){case 1:return function(e,t){if(!Object(ie.t)(t))throw new TypeError("Invalid index");{if(t.isScalar())return e.get(t.min());var r=t.size();if(r.length!==e._size.length)throw new D.a(r.length,e._size.length);for(var n=t.min(),i=t.max(),a=0,o=e._size.length;a<o;a++)Object(I.s)(n[a],e._size[a]),Object(I.s)(i[a],e._size[a]);return new m(function r(n,i,a,o){var e=o===a-1;var t=i.dimension(o);return e?t.map(function(e){return Object(I.s)(e,n.length),n[e]}).valueOf():t.map(function(e){Object(I.s)(e,n.length);var t=n[e];return r(t,i,a,o+1)}).valueOf()}(e._data,t,r.length,0),e._datatype)}}(this,e);case 2:case 3:return function(e,t,r,n){if(!t||!0!==t.isIndex)throw new TypeError("Invalid index");var i,a=t.size(),o=t.isScalar();Object(ie.v)(r)?(i=r.size(),r=r.valueOf()):i=Object(I.a)(r);if(o){if(0!==i.length)throw new TypeError("Scalar expected");e.set(t.min(),r,n)}else{if(a.length<e._size.length)throw new D.a(a.length,e._size.length,"<");if(i.length<a.length){for(var s=0,u=0;1===a[s]&&1===i[s];)s++;for(;1===a[s];)u++,s++;r=Object(I.q)(r,a.length,u,i)}if(!Object(ae.d)(a,i))throw new D.a(a,i,">");var c=t.max().map(function(e){return e+1});l(e,c,n);var f=a.length;!function r(n,i,a,o,s){var e=s===o-1;var t=i.dimension(s);e?t.forEach(function(e,t){Object(I.s)(e),n[e]=a[t[0]]}):t.forEach(function(e,t){Object(I.s)(e),r(n[e],i,a[t[0]],o,s+1)})}(e._data,t,r,f,0)}return e}(this,e,t,r);default:throw new SyntaxError("Wrong number of arguments")}},m.prototype.get=function(e){if(!Object(ie.b)(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new D.a(e.length,this._size.length);for(var t=0;t<e.length;t++)Object(I.s)(e[t],this._size[t]);for(var r=this._data,n=0,i=e.length;n<i;n++){var a=e[n];Object(I.s)(a,r.length),r=r[a]}return r},m.prototype.set=function(e,t,r){if(!Object(ie.b)(e))throw new TypeError("Array expected");if(e.length<this._size.length)throw new D.a(e.length,this._size.length,"<");var n,i,a;l(this,e.map(function(e){return e+1}),r);var o=this._data;for(n=0,i=e.length-1;n<i;n++)a=e[n],Object(I.s)(a,o.length),o=o[a];return a=e[e.length-1],Object(I.s)(a,o.length),o[a]=t,this},m.prototype.resize=function(e,t,r){if(!Object(ie.b)(e))throw new TypeError("Array expected");return s(r?this.clone():this,e,t)},m.prototype.reshape=function(e,t){var r=t?this.clone():this;return r._data=Object(I.n)(r._data,e),r._size=e.slice(0),r},m.prototype.clone=function(){return new m({data:Object(ae.a)(this._data),size:Object(ae.a)(this._size),datatype:this._datatype})},m.prototype.size=function(){return this._size.slice(0)},m.prototype.map=function(t){var i=this;return new m({data:function r(e,n){return Object(ie.b)(e)?e.map(function(e,t){return r(e,n.concat(t))}):t(e,n,i)}(this._data,[]),size:Object(ae.a)(this._size),datatype:this._datatype})},m.prototype.forEach=function(t){var i=this;!function r(e,n){Object(ie.b)(e)?e.forEach(function(e,t){r(e,n.concat(t))}):t(e,n,i)}(this._data,[])},m.prototype.toArray=function(){return Object(ae.a)(this._data)},m.prototype.valueOf=function(){return this._data},m.prototype.format=function(e){return Object(J.d)(this._data,e)},m.prototype.toString=function(){return Object(J.d)(this._data)},m.prototype.toJSON=function(){return{mathjs:"DenseMatrix",data:this._data,size:this._size,datatype:this._datatype}},m.prototype.diagonal=function(e){if(e){if(Object(ie.e)(e)&&(e=e.toNumber()),!Object(ie.y)(e)||!Object(j.i)(e))throw new TypeError("The parameter k must be an integer number")}else e=0;for(var t=0<e?e:0,r=e<0?-e:0,n=this._size[0],i=this._size[1],a=Math.min(n-r,i-t),o=[],s=0;s<a;s++)o[s]=this._data[s+r][s+t];return new m({data:o,size:[a],datatype:this._datatype})},m.diagonal=function(e,t,r,n){if(!Object(ie.b)(e))throw new TypeError("Array expected, size parameter");if(2!==e.length)throw new Error("Only two dimensions matrix are supported");if(e=e.map(function(e){if(Object(ie.e)(e)&&(e=e.toNumber()),!Object(ie.y)(e)||!Object(j.i)(e)||e<1)throw new Error("Size values must be positive integers");return e}),r){if(Object(ie.e)(r)&&(r=r.toNumber()),!Object(ie.y)(r)||!Object(j.i)(r))throw new TypeError("The parameter k must be an integer number")}else r=0;var i,a=0<r?r:0,o=r<0?-r:0,s=e[0],u=e[1],c=Math.min(s-o,u-a);if(Object(ie.b)(t)){if(t.length!==c)throw new Error("Invalid value array length");i=function(e){return t[e]}}else if(Object(ie.v)(t)){var f=t.size();if(1!==f.length||f[0]!==c)throw new Error("Invalid matrix length");i=function(e){return t.get([e])}}else i=function(){return t};n=n||(Object(ie.e)(i(0))?i(0).mul(0):0);var l=[];if(0<e.length){l=Object(I.o)(l,e,n);for(var p=0;p<c;p++)l[p+o][p+a]=i(p)}return new m({data:l,size:[s,u]})},m.fromJSON=function(e){return new m(e)},m.prototype.swapRows=function(e,t){if(!(Object(ie.y)(e)&&Object(j.i)(e)&&Object(ie.y)(t)&&Object(j.i)(t)))throw new Error("Row index must be positive integers");if(2!==this._size.length)throw new Error("Only two dimensional matrix is supported");return Object(I.s)(e,this._size[0]),Object(I.s)(t,this._size[0]),m._swapRows(e,t,this._data),this},m._swapRows=function(e,t,r){var n=r[e];r[e]=r[t],r[t]=n},m},{isClass:!0}),k=["typed"],z=Object(s.a)("clone",k,function(e){return(0,e.typed)("clone",{any:ae.a})}),R=r(10);function P(e){for(var t=0;t<e.length;t++)if(Object(ie.i)(e[t]))return!0;return!1}function F(e,t){Object(ie.v)(e)&&(e=e.valueOf());for(var r=0,n=e.length;r<n;r++){var i=e[r];Array.isArray(i)?F(i,t):t(i)}}function oe(e,t,r){return e&&"function"==typeof e.map?e.map(function(e){return oe(e,t,r)}):t(e)}function U(e,t,r){var n=Array.isArray(e)?Object(I.a)(e):e.size();if(t<0||t>=n.length)throw new R.a(t,n.length);return Object(ie.v)(e)?e.create(L(e.valueOf(),t,r)):L(e,t,r)}function L(e,t,r){var n,i,a,o;if(t<=0){if(Array.isArray(e[0])){for(o=function(e){var t,r,n=e.length,i=e[0].length,a=[];for(r=0;r<i;r++){var o=[];for(t=0;t<n;t++)o.push(e[t][r]);a.push(o)}return a}(e),i=[],n=0;n<o.length;n++)i[n]=L(o[n],t-1,r);return i}for(a=e[0],n=1;n<e.length;n++)a=r(a,e[n]);return a}for(i=[],n=0;n<e.length;n++)i[n]=L(e[n],t-1,r);return i}function H(e,t,r,n,i,a,o,s,u,c,f){var l,p,m,h,d=e._values,y=e._index,g=e._ptr,v=o._index;if(n)for(p=g[t],m=g[t+1],l=p;l<m;l++)r[h=y[l]]!==a?(r[h]=a,v.push(h),c?(n[h]=u?s(d[l],f):s(f,d[l]),i[h]=a):n[h]=d[l]):(n[h]=u?s(d[l],n[h]):s(n[h],d[l]),i[h]=a);else for(p=g[t],m=g[t+1],l=p;l<m;l++)r[h=y[l]]!==a?(r[h]=a,v.push(h)):i[h]=a}var $="isInteger",G=["typed"],Z=Object(s.a)($,G,function(e){var t=(0,e.typed)($,{number:j.i,BigNumber:function(e){return e.isInt()},Fraction:function(e){return 1===e.d&&isFinite(e.n)},"Array | Matrix":function(e){return oe(e,t)}});return t}),V="number";function W(e){return e<0}function Y(e){return 0<e}function X(e){return 0===e}function Q(e){return Number.isNaN(e)}Q.signature=X.signature=Y.signature=W.signature=V;var K="isNegative",ee=["typed"],te=Object(s.a)(K,ee,function(e){var t=(0,e.typed)(K,{number:W,BigNumber:function(e){return e.isNeg()&&!e.isZero()&&!e.isNaN()},Fraction:function(e){return e.s<0},Unit:function(e){return t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),re="isNumeric",ne=["typed"],se=Object(s.a)(re,ne,function(e){var t=(0,e.typed)(re,{"number | BigNumber | Fraction | boolean":function(){return!0},"Complex | Unit | string | null | undefined | Node":function(){return!1},"Array | Matrix":function(e){return oe(e,t)}});return t}),ue="hasNumericValue",ce=["typed","isNumeric"],fe=Object(s.a)(ue,ce,function(e){var t=e.typed,r=e.isNumeric;return t(ue,{string:function(e){return 0<e.trim().length&&!isNaN(Number(e))},any:function(e){return r(e)}})}),le="isPositive",pe=["typed"],me=Object(s.a)(le,pe,function(e){var t=(0,e.typed)(le,{number:Y,BigNumber:function(e){return!e.isNeg()&&!e.isZero()&&!e.isNaN()},Fraction:function(e){return 0<e.s&&0<e.n},Unit:function(e){return t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),he=["typed"],de=Object(s.a)("isZero",he,function(e){var t=(0,e.typed)("isZero",{number:X,BigNumber:function(e){return e.isZero()},Complex:function(e){return 0===e.re&&0===e.im},Fraction:function(e){return 1===e.d&&0===e.n},Unit:function(e){return t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),ye=["typed"],ge=Object(s.a)("isNaN",ye,function(e){return(0,e.typed)("isNaN",{number:Q,BigNumber:function(e){return e.isNaN()},Fraction:function(e){return!1},Complex:function(e){return e.isNaN()},Unit:function(e){return Number.isNaN(e.value)},"Array | Matrix":function(e){return oe(e,Number.isNaN)}})}),ve=r(8),be=["typed"],xe=Object(s.a)("typeOf",be,function(e){return(0,e.typed)("typeOf",{any:ie.M})}),we=Object(s.a)("typeof",[],function(){return function(){Object(ve.a)('Function "typeof" has been renamed to "typeOf" in v6.0.0, please use the new function instead.');for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return ie.M.apply(ie.M,t)}});function Ne(e,t,r){if(null==r)return e.eq(t);if(e.eq(t))return!0;if(e.isNaN()||t.isNaN())return!1;if(e.isFinite()&&t.isFinite()){var n=e.minus(t).abs();if(n.isZero())return!0;var i=e.constructor.max(e.abs(),t.abs());return n.lte(i.times(r))}return!1}var Oe="equalScalar",Me=["typed","config"],Ee=Object(s.a)(Oe,Me,function(e){var t=e.typed,r=e.config,n=t(Oe,{"boolean, boolean":function(e,t){return e===t},"number, number":function(e,t){return Object(j.m)(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.eq(t)||Ne(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return e.equals(t)},"Complex, Complex":function(e,t){return function(e,t,r){return Object(j.m)(e.re,t.re,r)&&Object(j.m)(e.im,t.im,r)}(e,t,r.epsilon)},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return n(e.value,t.value)}});return n}),je=(Object(s.a)(Oe,["typed","config"],function(e){var t=e.typed,r=e.config;return t(Oe,{"number, number":function(e,t){return Object(j.m)(e,t,r.epsilon)}})}),["typed","equalScalar","Matrix"]),Se=Object(s.a)("SparseMatrix",je,function(e){var O=e.typed,M=e.equalScalar,t=e.Matrix;function E(e,t){if(!(this instanceof E))throw new SyntaxError("Constructor must be called with the new operator");if(t&&!Object(ie.I)(t))throw new Error("Invalid datatype: "+t);if(Object(ie.v)(e))!function(e,t,r){"SparseMatrix"===t.type?(e._values=t._values?Object(ae.a)(t._values):void 0,e._index=Object(ae.a)(t._index),e._ptr=Object(ae.a)(t._ptr),e._size=Object(ae.a)(t._size),e._datatype=r||t._datatype):n(e,t.valueOf(),r||t._datatype)}(this,e,t);else if(e&&Object(ie.b)(e.index)&&Object(ie.b)(e.ptr)&&Object(ie.b)(e.size))this._values=e.values,this._index=e.index,this._ptr=e.ptr,this._size=e.size,this._datatype=t||e.datatype;else if(Object(ie.b)(e))n(this,e,t);else{if(e)throw new TypeError("Unsupported type of data ("+Object(ie.M)(e)+")");this._values=[],this._index=[],this._ptr=[0],this._size=[0,0],this._datatype=t}}function n(e,t,r){e._values=[],e._index=[],e._ptr=[],e._datatype=r;var n=t.length,i=0,a=M,o=0;if(Object(ie.I)(r)&&(a=O.find(M,[r,r])||M,o=O.convert(0,r)),0<n){var s=0;do{e._ptr.push(e._index.length);for(var u=0;u<n;u++){var c=t[u];if(Object(ie.b)(c)){if(0===s&&i<c.length&&(i=c.length),s<c.length){var f=c[s];a(f,o)||(e._values.push(f),e._index.push(u))}}else 0===s&&i<1&&(i=1),a(c,o)||(e._values.push(c),e._index.push(u))}s++}while(s<i)}e._ptr.push(e._index.length),e._size=[n,i]}function g(e,t,r,n){if(r-t==0)return r;for(var i=t;i<r;i++)if(n[i]===e)return i;return t}function v(e,t,r,n,i,a,o){i.splice(e,0,n),a.splice(e,0,t);for(var s=r+1;s<o.length;s++)o[s]++}function f(e,t,r,n){var i=n||0,a=M,o=0;Object(ie.I)(e._datatype)&&(a=O.find(M,[e._datatype,e._datatype])||M,o=O.convert(0,e._datatype),i=O.convert(i,e._datatype));var s,u,c,f=!a(i,o),l=e._size[0],p=e._size[1];if(p<r){for(u=p;u<r;u++)if(e._ptr[u]=e._values.length,f)for(s=0;s<l;s++)e._values.push(i),e._index.push(s);e._ptr[r]=e._values.length}else r<p&&(e._ptr.splice(r+1,p-r),e._values.splice(e._ptr[r],e._values.length),e._index.splice(e._ptr[r],e._index.length));if(p=r,l<t){if(f){var m=0;for(u=0;u<p;u++){e._ptr[u]=e._ptr[u]+m,c=e._ptr[u+1]+m;var h=0;for(s=l;s<t;s++,h++)e._values.splice(c+h,0,i),e._index.splice(c+h,0,s),m++}e._ptr[p]=e._values.length}}else if(t<l){var d=0;for(u=0;u<p;u++){e._ptr[u]=e._ptr[u]-d;var y=e._ptr[u],g=e._ptr[u+1]-d;for(c=y;c<g;c++)t-1<(s=e._index[c])&&(e._values.splice(c,1),e._index.splice(c,1),d++)}e._ptr[u]=e._values.length}return e._size[0]=t,e._size[1]=r,e}function r(e,t,r,n,i){var a,o,s=n[0],u=n[1],c=[];for(a=0;a<s;a++)for(c[a]=[],o=0;o<u;o++)c[a][o]=0;for(o=0;o<u;o++)for(var f=r[o],l=r[o+1],p=f;p<l;p++)c[a=t[p]][o]=e?i?Object(ae.a)(e[p]):e[p]:1;return c}return(E.prototype=new t).createSparseMatrix=function(e,t){return new E(e,t)},E.prototype.type="SparseMatrix",E.prototype.isSparseMatrix=!0,E.prototype.getDataType=function(){return Object(I.h)(this._values,ie.M)},E.prototype.storage=function(){return"sparse"},E.prototype.datatype=function(){return this._datatype},E.prototype.create=function(e,t){return new E(e,t)},E.prototype.density=function(){var e=this._size[0],t=this._size[1];return 0!==e&&0!==t?this._index.length/(e*t):0},E.prototype.subset=function(e,t,r){if(!this._values)throw new Error("Cannot invoke subset on a Pattern only matrix");switch(arguments.length){case 1:return function(e,t){if(!Object(ie.t)(t))throw new TypeError("Invalid index");if(t.isScalar())return e.get(t.min());var r,n,i,a,o=t.size();if(o.length!==e._size.length)throw new D.a(o.length,e._size.length);var s=t.min(),u=t.max();for(r=0,n=e._size.length;r<n;r++)Object(I.s)(s[r],e._size[r]),Object(I.s)(u[r],e._size[r]);var c=e._values,f=e._index,l=e._ptr,p=t.dimension(0),m=t.dimension(1),h=[],d=[];p.forEach(function(e,t){d[e]=t[0],h[e]=!0});var y=c?[]:void 0,g=[],v=[];return m.forEach(function(e){for(v.push(g.length),i=l[e],a=l[e+1];i<a;i++)r=f[i],!0===h[r]&&(g.push(d[r]),y&&y.push(c[i]))}),v.push(g.length),new E({values:y,index:g,ptr:v,size:o,datatype:e._datatype})}(this,e);case 2:case 3:return function(e,t,r,n){if(!t||!0!==t.isIndex)throw new TypeError("Invalid index");var i,a=t.size(),o=t.isScalar();Object(ie.v)(r)?(i=r.size(),r=r.toArray()):i=Object(I.a)(r);if(o){if(0!==i.length)throw new TypeError("Scalar expected");e.set(t.min(),r,n)}else{if(1!==a.length&&2!==a.length)throw new D.a(a.length,e._size.length,"<");if(i.length<a.length){for(var s=0,u=0;1===a[s]&&1===i[s];)s++;for(;1===a[s];)u++,s++;r=Object(I.q)(r,a.length,u,i)}if(!Object(ae.d)(a,i))throw new D.a(a,i,">");for(var c=t.min()[0],f=t.min()[1],l=i[0],p=i[1],m=0;m<l;m++)for(var h=0;h<p;h++){var d=r[m][h];e.set([m+c,h+f],d,n)}}return e}(this,e,t,r);default:throw new SyntaxError("Wrong number of arguments")}},E.prototype.get=function(e){if(!Object(ie.b)(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new D.a(e.length,this._size.length);if(!this._values)throw new Error("Cannot invoke get on a Pattern only matrix");var t=e[0],r=e[1];Object(I.s)(t,this._size[0]),Object(I.s)(r,this._size[1]);var n=g(t,this._ptr[r],this._ptr[r+1],this._index);return n<this._ptr[r+1]&&this._index[n]===t?this._values[n]:0},E.prototype.set=function(e,t,r){if(!Object(ie.b)(e))throw new TypeError("Array expected");if(e.length!==this._size.length)throw new D.a(e.length,this._size.length);if(!this._values)throw new Error("Cannot invoke set on a Pattern only matrix");var n=e[0],i=e[1],a=this._size[0],o=this._size[1],s=M,u=0;Object(ie.I)(this._datatype)&&(s=O.find(M,[this._datatype,this._datatype])||M,u=O.convert(0,this._datatype)),(a-1<n||o-1<i)&&(f(this,Math.max(n+1,a),Math.max(i+1,o),r),a=this._size[0],o=this._size[1]),Object(I.s)(n,a),Object(I.s)(i,o);var c=g(n,this._ptr[i],this._ptr[i+1],this._index);return c<this._ptr[i+1]&&this._index[c]===n?s(t,u)?function(e,t,r,n,i){r.splice(e,1),n.splice(e,1);for(var a=t+1;a<i.length;a++)i[a]--}(c,i,this._values,this._index,this._ptr):this._values[c]=t:v(c,n,i,t,this._values,this._index,this._ptr),this},E.prototype.resize=function(t,e,r){if(!Object(ie.b)(t))throw new TypeError("Array expected");if(2!==t.length)throw new Error("Only two dimensions matrix are supported");return t.forEach(function(e){if(!Object(ie.y)(e)||!Object(j.i)(e)||e<0)throw new TypeError("Invalid size, must contain positive integers (size: "+Object(J.d)(t)+")")}),f(r?this.clone():this,t[0],t[1],e)},E.prototype.reshape=function(t,e){if(!Object(ie.b)(t))throw new TypeError("Array expected");if(2!==t.length)throw new Error("Sparse matrices can only be reshaped in two dimensions");if(t.forEach(function(e){if(!Object(ie.y)(e)||!Object(j.i)(e)||e<0)throw new TypeError("Invalid size, must contain positive integers (size: "+Object(J.d)(t)+")")}),this._size[0]*this._size[1]!=t[0]*t[1])throw new Error("Reshaping sparse matrix will result in the wrong number of elements");var r=e?this.clone():this;if(this._size[0]===t[0]&&this._size[1]===t[1])return r;for(var n=[],i=0;i<r._ptr.length;i++)for(var a=0;a<r._ptr[i+1]-r._ptr[i];a++)n.push(i);for(var o=r._values.slice(),s=r._index.slice(),u=0;u<r._index.length;u++){var c=s[u],f=n[u],l=c*r._size[1]+f;n[u]=l%t[1],s[u]=Math.floor(l/t[1])}r._values.length=0,r._index.length=0,r._ptr.length=t[1]+1,r._size=t.slice();for(var p=0;p<r._ptr.length;p++)r._ptr[p]=0;for(var m=0;m<o.length;m++){var h=s[m],d=n[m],y=o[m];v(g(h,r._ptr[d],r._ptr[d+1],r._index),h,d,y,r._values,r._index,r._ptr)}return r},E.prototype.clone=function(){return new E({values:this._values?Object(ae.a)(this._values):void 0,index:Object(ae.a)(this._index),ptr:Object(ae.a)(this._ptr),size:Object(ae.a)(this._size),datatype:this._datatype})},E.prototype.size=function(){return this._size.slice(0)},E.prototype.map=function(n,e){if(!this._values)throw new Error("Cannot invoke map on a Pattern only matrix");var i=this;return function(e,t,r,n,i,a,o){var s=[],u=[],c=[],f=M,l=0;Object(ie.I)(e._datatype)&&(f=O.find(M,[e._datatype,e._datatype])||M,l=O.convert(0,e._datatype));for(var p=function(e,t,r){e=a(e,t,r),f(e,l)||(s.push(e),u.push(t))},m=n;m<=i;m++){c.push(s.length);var h=e._ptr[m],d=e._ptr[m+1];if(o)for(var y=h;y<d;y++){var g=e._index[y];t<=g&&g<=r&&p(e._values[y],g-t,m-n)}else{for(var v={},b=h;b<d;b++){var x=e._index[b];v[x]=e._values[b]}for(var w=t;w<=r;w++){var N=w in v?v[w]:0;p(N,w-t,m-n)}}}return c.push(s.length),new E({values:s,index:u,ptr:c,size:[r-t+1,i-n+1]})}(this,0,this._size[0]-1,0,this._size[1]-1,function(e,t,r){return n(e,[t,r],i)},e)},E.prototype.forEach=function(e,t){if(!this._values)throw new Error("Cannot invoke forEach on a Pattern only matrix");for(var r=this._size[0],n=this._size[1],i=0;i<n;i++){var a=this._ptr[i],o=this._ptr[i+1];if(t)for(var s=a;s<o;s++){var u=this._index[s];e(this._values[s],[u,i],this)}else{for(var c={},f=a;f<o;f++){c[this._index[f]]=this._values[f]}for(var l=0;l<r;l++){e(l in c?c[l]:0,[l,i],this)}}}},E.prototype.toArray=function(){return r(this._values,this._index,this._ptr,this._size,!0)},E.prototype.valueOf=function(){return r(this._values,this._index,this._ptr,this._size,!1)},E.prototype.format=function(e){for(var t=this._size[0],r=this._size[1],n=this.density(),i="Sparse Matrix ["+Object(J.d)(t,e)+" x "+Object(J.d)(r,e)+"] density: "+Object(J.d)(n,e)+"\n",a=0;a<r;a++)for(var o=this._ptr[a],s=this._ptr[a+1],u=o;u<s;u++){var c=this._index[u];i+="\n    ("+Object(J.d)(c,e)+", "+Object(J.d)(a,e)+") ==> "+(this._values?Object(J.d)(this._values[u],e):"X")}return i},E.prototype.toString=function(){return Object(J.d)(this.toArray())},E.prototype.toJSON=function(){return{mathjs:"SparseMatrix",values:this._values,index:this._index,ptr:this._ptr,size:this._size,datatype:this._datatype}},E.prototype.diagonal=function(e){if(e){if(Object(ie.e)(e)&&(e=e.toNumber()),!Object(ie.y)(e)||!Object(j.i)(e))throw new TypeError("The parameter k must be an integer number")}else e=0;var t=0<e?e:0,r=e<0?-e:0,n=this._size[0],i=this._size[1],a=Math.min(n-r,i-t),o=[],s=[],u=[];u[0]=0;for(var c=t;c<i&&o.length<a;c++)for(var f=this._ptr[c],l=this._ptr[c+1],p=f;p<l;p++){var m=this._index[p];if(m===c-t+r){o.push(this._values[p]),s[o.length-1]=m-r;break}}return u.push(o.length),new E({values:o,index:s,ptr:u,size:[a,1]})},E.fromJSON=function(e){return new E(e)},E.diagonal=function(e,t,r,n,i){if(!Object(ie.b)(e))throw new TypeError("Array expected, size parameter");if(2!==e.length)throw new Error("Only two dimensions matrix are supported");if(e=e.map(function(e){if(Object(ie.e)(e)&&(e=e.toNumber()),!Object(ie.y)(e)||!Object(j.i)(e)||e<1)throw new Error("Size values must be positive integers");return e}),r){if(Object(ie.e)(r)&&(r=r.toNumber()),!Object(ie.y)(r)||!Object(j.i)(r))throw new TypeError("The parameter k must be an integer number")}else r=0;var a=M,o=0;Object(ie.I)(i)&&(a=O.find(M,[i,i])||M,o=O.convert(0,i));var s,u=0<r?r:0,c=r<0?-r:0,f=e[0],l=e[1],p=Math.min(f-c,l-u);if(Object(ie.b)(t)){if(t.length!==p)throw new Error("Invalid value array length");s=function(e){return t[e]}}else if(Object(ie.v)(t)){var m=t.size();if(1!==m.length||m[0]!==p)throw new Error("Invalid matrix length");s=function(e){return t.get([e])}}else s=function(){return t};for(var h=[],d=[],y=[],g=0;g<l;g++){y.push(h.length);var v=g-u;if(0<=v&&v<p){var b=s(v);a(b,o)||(d.push(v+c),h.push(b))}}return y.push(h.length),new E({values:h,index:d,ptr:y,size:[f,l]})},E.prototype.swapRows=function(e,t){if(!(Object(ie.y)(e)&&Object(j.i)(e)&&Object(ie.y)(t)&&Object(j.i)(t)))throw new Error("Row index must be positive integers");if(2!==this._size.length)throw new Error("Only two dimensional matrix is supported");return Object(I.s)(e,this._size[0]),Object(I.s)(t,this._size[0]),E._swapRows(e,t,this._size[1],this._values,this._index,this._ptr),this},E._forEachRow=function(e,t,r,n,i){for(var a=n[e],o=n[e+1],s=a;s<o;s++)i(r[s],t[s])},E._swapRows=function(e,t,r,n,i,a){for(var o=0;o<r;o++){var s=a[o],u=a[o+1],c=g(e,s,u,i),f=g(t,s,u,i);if(c<u&&f<u&&i[c]===e&&i[f]===t){if(n){var l=n[c];n[c]=n[f],n[f]=l}}else if(c<u&&i[c]===e&&(u<=f||i[f]!==t)){var p=n?n[c]:void 0;i.splice(f,0,t),n&&n.splice(f,0,p),i.splice(f<=c?c+1:c,1),n&&n.splice(f<=c?c+1:c,1)}else if(f<u&&i[f]===t&&(u<=c||i[c]!==e)){var m=n?n[f]:void 0;i.splice(c,0,e),n&&n.splice(c,0,m),i.splice(c<=f?f+1:f,1),n&&n.splice(c<=f?f+1:f,1)}}},E},{isClass:!0}),Ae=["typed"],Ce=Object(s.a)("number",Ae,function(e){var t=(0,e.typed)("number",{"":function(){return 0},number:function(e){return e},string:function(e){if("NaN"===e)return NaN;var t=Number(e);if(isNaN(t))throw new SyntaxError('String "'+e+'" is no valid number');return t},BigNumber:function(e){return e.toNumber()},Fraction:function(e){return e.valueOf()},Unit:function(e){throw new Error("Second argument with valueless unit expected")},null:function(e){return 0},"Unit, string | Unit":function(e,t){return e.toNumber(t)},"Array | Matrix":function(e){return oe(e,t)}});return t}),Te=["typed"],_e=Object(s.a)("string",Te,function(e){var t=(0,e.typed)("string",{"":function(){return""},number:j.h,null:function(e){return"null"},boolean:function(e){return e+""},string:function(e){return e},"Array | Matrix":function(e){return oe(e,t)},any:function(e){return String(e)}});return t}),Ie="boolean",qe=["typed"],Be=Object(s.a)(Ie,qe,function(e){var t=(0,e.typed)(Ie,{"":function(){return!1},boolean:function(e){return e},number:function(e){return!!e},null:function(e){return!1},BigNumber:function(e){return!e.isZero()},string:function(e){var t=e.toLowerCase();if("true"===t)return!0;if("false"===t)return!1;var r=Number(e);if(""!==e&&!isNaN(r))return!!r;throw new Error('Cannot convert "'+e+'" to a boolean')},"Array | Matrix":function(e){return oe(e,t)}});return t}),ke=["typed","BigNumber"],ze=Object(s.a)("bignumber",ke,function(e){var t=e.typed,r=e.BigNumber,n=t("bignumber",{"":function(){return new r(0)},number:function(e){return new r(e+"")},string:function(e){return new r(e)},BigNumber:function(e){return e},Fraction:function(e){return new r(e.n).div(e.d).times(e.s)},null:function(e){return new r(0)},"Array | Matrix":function(e){return oe(e,n)}});return n}),De=["typed","Complex"],Re=Object(s.a)("complex",De,function(e){var t=e.typed,r=e.Complex,n=t("complex",{"":function(){return r.ZERO},number:function(e){return new r(e,0)},"number, number":function(e,t){return new r(e,t)},"BigNumber, BigNumber":function(e,t){return new r(e.toNumber(),t.toNumber())},Fraction:function(e){return new r(e.valueOf(),0)},Complex:function(e){return e.clone()},string:function(e){return r(e)},null:function(e){return r(0)},Object:function(e){if("re"in e&&"im"in e)return new r(e.re,e.im);if("r"in e&&"phi"in e||"abs"in e&&"arg"in e)return new r(e);throw new Error("Expected object with properties (re and im) or (r and phi) or (abs and arg)")},"Array | Matrix":function(e){return oe(e,n)}});return n}),Pe=["typed","Fraction"],Fe=Object(s.a)("fraction",Pe,function(e){var t=e.typed,r=e.Fraction,n=t("fraction",{number:function(e){if(!isFinite(e)||isNaN(e))throw new Error(e+" cannot be represented as a fraction");return new r(e)},string:function(e){return new r(e)},"number, number":function(e,t){return new r(e,t)},null:function(e){return new r(0)},BigNumber:function(e){return new r(e.toString())},Fraction:function(e){return e},Object:function(e){return new r(e)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Ue=["typed","Matrix","DenseMatrix","SparseMatrix"],Le=Object(s.a)("matrix",Ue,function(e){var t=e.typed,n=(e.Matrix,e.DenseMatrix),i=e.SparseMatrix;return t("matrix",{"":function(){return r([])},string:function(e){return r([],e)},"string, string":function(e,t){return r([],e,t)},Array:function(e){return r(e)},Matrix:function(e){return r(e,e.storage())},"Array | Matrix, string":r,"Array | Matrix, string, string":r});function r(e,t,r){if("dense"===t||"default"===t||void 0===t)return new n(e,r);if("sparse"===t)return new i(e,r);throw new TypeError("Unknown matrix type "+JSON.stringify(t)+".")}}),He="splitUnit",$e=["typed"],Ge=Object(s.a)(He,$e,function(e){return(0,e.typed)(He,{"Unit, Array":function(e,t){return e.splitUnit(t)}})}),Ze="number",Ve="number, number";function Je(e){return Math.abs(e)}function We(e,t){return e+t}function Ye(e,t){return e*t}function Xe(e){return-e}function Qe(e){return e}function Ke(e){return Object(j.d)(e)}function et(e){return Math.ceil(e)}function tt(e){return e*e*e}function rt(e){return Math.exp(e)}function nt(e){return Object(j.g)(e)}function it(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Parameters in function gcd must be integer numbers");for(var r;0!==t;)r=e%t,e=t,t=r;return e<0?-e:e}function at(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Parameters in function lcm must be integer numbers");if(0===e||0===t)return 0;for(var r,n=e*t;0!==t;)t=e%(r=t),e=r;return Math.abs(n/e)}function ot(e){return Math.log(e)}function st(e){return Object(j.j)(e)}function ut(e){return Object(j.l)(e)}function ct(e,t){if(0<t)return e-t*Math.floor(e/t);if(0===t)return e;throw new Error("Cannot calculate mod for a negative divisor")}function ft(e,t){var r=t<0;if(r&&(t=-t),0===t)throw new Error("Root must be non-zero");if(e<0&&Math.abs(t)%2!=1)throw new Error("Root must be odd when a is negative.");if(0===e)return r?1/0:0;if(!isFinite(e))return r?0:e;var n=Math.pow(Math.abs(e),1/t);return n=e<0?-n:n,r?1/n:n}function lt(e){return Object(j.n)(e)}function pt(e){return e*e}function mt(e,t){var r,n,i,a=0,o=1,s=1,u=0;if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Parameters in function xgcd must be integer numbers");for(;t;)i=e-(n=Math.floor(e/t))*t,a=o-n*(r=a),o=r,s=u-n*(r=s),u=r,e=t,t=i;return e<0?[-e,-o,-u]:[e,e?o:0,u]}function ht(e,t){return e*e<1&&t===1/0||1<e*e&&t===-1/0?0:Math.pow(e,t)}function dt(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:0;return parseFloat(Object(j.q)(e,t))}Je.signature=Ze,Ye.signature=We.signature=Ve,nt.signature=rt.signature=tt.signature=et.signature=Ke.signature=Qe.signature=Xe.signature=Ze,at.signature=it.signature=Ve,ut.signature=st.signature=ot.signature=Ze,ft.signature=ct.signature=Ve,pt.signature=lt.signature=Ze,dt.signature=ht.signature=mt.signature=Ve;var yt="unaryMinus",gt=["typed"],vt=Object(s.a)(yt,gt,function(e){var r=(0,e.typed)(yt,{number:Xe,Complex:function(e){return e.neg()},BigNumber:function(e){return e.neg()},Fraction:function(e){return e.neg()},Unit:function(e){var t=e.clone();return t.value=r(e.value),t},"Array | Matrix":function(e){return oe(e,r,!0)}});return r}),bt="unaryPlus",xt=["typed","config","BigNumber"],wt=Object(s.a)(bt,xt,function(e){var t=e.typed,r=e.config,n=e.BigNumber,i=t(bt,{number:Qe,Complex:function(e){return e},BigNumber:function(e){return e},Fraction:function(e){return e},Unit:function(e){return e.clone()},"Array | Matrix":function(e){return oe(e,i,!0)},"boolean | string":function(e){return"BigNumber"===r.number?new n(+e):+e}});return i}),Nt=["typed"],Ot=Object(s.a)("abs",Nt,function(e){var t=(0,e.typed)("abs",{number:Je,Complex:function(e){return e.abs()},BigNumber:function(e){return e.abs()},Fraction:function(e){return e.abs()},"Array | Matrix":function(e){return oe(e,t,!0)},Unit:function(e){return e.abs()}});return t}),Mt=["typed","isInteger"],Et=Object(s.a)("apply",Mt,function(e){var t=e.typed,i=e.isInteger;return t("apply",{"Array | Matrix, number | BigNumber, function":function(e,t,r){if(!i(t))throw new TypeError("Integer number expected for dimension");var n=Array.isArray(e)?Object(I.a)(e):e.size();if(t<0||t>=n.length)throw new R.a(t,n.length);return Object(ie.v)(e)?e.create(jt(e.valueOf(),t,r)):jt(e,t,r)}})});function jt(e,t,r){var n,i,a;if(t<=0){if(Array.isArray(e[0])){for(a=function(e){var t,r,n=e.length,i=e[0].length,a=[];for(r=0;r<i;r++){var o=[];for(t=0;t<n;t++)o.push(e[t][r]);a.push(o)}return a}(e),i=[],n=0;n<a.length;n++)i[n]=jt(a[n],t-1,r);return i}return r(e)}for(i=[],n=0;n<e.length;n++)i[n]=jt(e[n],t-1,r);return i}var St="addScalar",At=["typed"],Ct=Object(s.a)(St,At,function(e){var n=(0,e.typed)(St,{"number, number":We,"Complex, Complex":function(e,t){return e.add(t)},"BigNumber, BigNumber":function(e,t){return e.plus(t)},"Fraction, Fraction":function(e,t){return e.add(t)},"Unit, Unit":function(e,t){if(null===e.value||void 0===e.value)throw new Error("Parameter x contains a unit with undefined value");if(null===t.value||void 0===t.value)throw new Error("Parameter y contains a unit with undefined value");if(!e.equalBase(t))throw new Error("Units do not match");var r=e.clone();return r.value=n(r.value,t.value),r.fixPrefix=!1,r}});return n}),Tt=["config","typed","isNegative","unaryMinus","matrix","Complex","BigNumber","Fraction"],_t=Object(s.a)("cbrt",Tt,function(e){var o=e.config,t=e.typed,a=e.isNegative,s=e.unaryMinus,u=e.matrix,c=e.Complex,f=e.BigNumber,l=e.Fraction,r=t("cbrt",{number:Ke,Complex:p,"Complex, boolean":p,BigNumber:function(e){return e.cbrt()},Unit:function(e){{if(e.value&&Object(ie.j)(e.value)){var t=e.clone();return t.value=1,(t=t.pow(1/3)).value=p(e.value),t}var r,n=a(e.value);n&&(e.value=s(e.value)),r=Object(ie.e)(e.value)?new f(1).div(3):Object(ie.o)(e.value)?new l(1,3):1/3;var i=e.pow(r);return n&&(i.value=s(i.value)),i}},"Array | Matrix":function(e){return oe(e,r,!0)}});function p(e,t){var r=e.arg()/3,n=e.abs(),i=new c(Ke(n),0).mul(new c(0,r).exp());if(t){var a=[i,new c(Ke(n),0).mul(new c(0,r+2*Math.PI/3).exp()),new c(Ke(n),0).mul(new c(0,r-2*Math.PI/3).exp())];return"Array"===o.matrix?a:u(a)}return i}return r}),It=["typed","config","round"],qt=Object(s.a)("ceil",It,function(e){var t=e.typed,r=e.config,n=e.round,i=t("ceil",{number:function(e){return Object(j.m)(e,n(e),r.epsilon)?n(e):et(e)},Complex:function(e){return e.ceil()},BigNumber:function(e){return Ne(e,n(e),r.epsilon)?n(e):e.ceil()},Fraction:function(e){return e.ceil()},"Array | Matrix":function(e){return oe(e,i,!0)}});return i}),Bt=["typed"],kt=Object(s.a)("cube",Bt,function(e){var t=(0,e.typed)("cube",{number:tt,Complex:function(e){return e.mul(e).mul(e)},BigNumber:function(e){return e.times(e).times(e)},Fraction:function(e){return e.pow(3)},"Array | Matrix":function(e){return oe(e,t,!0)},Unit:function(e){return e.pow(3)}});return t}),zt=["typed"],Dt=Object(s.a)("exp",zt,function(e){var t=(0,e.typed)("exp",{number:rt,Complex:function(e){return e.exp()},BigNumber:function(e){return e.exp()},"Array | Matrix":function(e){return oe(e,t)}});return t}),Rt=["typed","Complex"],Pt=Object(s.a)("expm1",Rt,function(e){var t=e.typed,r=e.Complex,n=t("expm1",{number:nt,Complex:function(e){var t=Math.exp(e.re);return new r(t*Math.cos(e.im)-1,t*Math.sin(e.im))},BigNumber:function(e){return e.exp().minus(1)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Ft=["typed","Complex","ceil","floor"],Ut=Object(s.a)("fix",Ft,function(e){var t=e.typed,r=e.Complex,n=e.ceil,i=e.floor,a=t("fix",{number:function(e){return 0<e?i(e):n(e)},Complex:function(e){return new r(0<e.re?Math.floor(e.re):Math.ceil(e.re),0<e.im?Math.floor(e.im):Math.ceil(e.im))},BigNumber:function(e){return e.isNegative()?n(e):i(e)},Fraction:function(e){return e.s<0?e.ceil():e.floor()},"Array | Matrix":function(e){return oe(e,a,!0)}});return a}),Lt=["typed","config","round"],Ht=Object(s.a)("floor",Lt,function(e){var t=e.typed,r=e.config,n=e.round,i=t("floor",{number:function(e){return Object(j.m)(e,n(e),r.epsilon)?n(e):Math.floor(e)},Complex:function(e){return e.floor()},BigNumber:function(e){return Ne(e,n(e),r.epsilon)?n(e):e.floor()},Fraction:function(e){return e.floor()},"Array | Matrix":function(e){return oe(e,i,!0)}});return i}),$t=["typed"],Gt=Object(s.a)("algorithm01",$t,function(e){var E=e.typed;return function(e,t,r,n){var i=e._data,a=e._size,o=e._datatype,s=t._values,u=t._index,c=t._ptr,f=t._size,l=t._datatype;if(a.length!==f.length)throw new D.a(a.length,f.length);if(a[0]!==f[0]||a[1]!==f[1])throw new RangeError("Dimension mismatch. Matrix A ("+a+") must match Matrix B ("+f+")");if(!s)throw new Error("Cannot perform operation on Dense Matrix and Pattern Sparse Matrix");var p,m,h=a[0],d=a[1],y="string"==typeof o&&o===l?o:void 0,g=y?E.find(r,[y,y]):r,v=[];for(p=0;p<h;p++)v[p]=[];var b=[],x=[];for(m=0;m<d;m++){for(var w=m+1,N=c[m],O=c[m+1],M=N;M<O;M++)b[p=u[M]]=n?g(s[M],i[p][m]):g(i[p][m],s[M]),x[p]=w;for(p=0;p<h;p++)x[p]===w?v[p][m]=b[p]:v[p][m]=i[p][m]}return e.createDenseMatrix({data:v,size:[h,d],datatype:y})}}),Zt=["typed","equalScalar"],Vt=Object(s.a)("algorithm04",Zt,function(e){var B=e.typed,k=e.equalScalar;return function(e,t,r){var n=e._values,i=e._index,a=e._ptr,o=e._size,s=e._datatype,u=t._values,c=t._index,f=t._ptr,l=t._size,p=t._datatype;if(o.length!==l.length)throw new D.a(o.length,l.length);if(o[0]!==l[0]||o[1]!==l[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+l+")");var m,h=o[0],d=o[1],y=k,g=0,v=r;"string"==typeof s&&s===p&&(m=s,y=B.find(k,[m,m]),g=B.convert(0,m),v=B.find(r,[m,m]));var b,x,w,N,O,M=n&&u?[]:void 0,E=[],j=[],S=e.createSparseMatrix({values:M,index:E,ptr:j,size:[h,d],datatype:m}),A=n&&u?[]:void 0,C=n&&u?[]:void 0,T=[],_=[];for(x=0;x<d;x++){j[x]=E.length;var I=x+1;for(N=a[x],O=a[x+1],w=N;w<O;w++)b=i[w],E.push(b),T[b]=I,A&&(A[b]=n[w]);for(N=f[x],O=f[x+1],w=N;w<O;w++)if(T[b=c[w]]===I){if(A){var q=v(A[b],u[w]);y(q,g)?T[b]=null:A[b]=q}}else E.push(b),_[b]=I,C&&(C[b]=u[w]);if(A&&C)for(w=j[x];w<E.length;)T[b=E[w]]===I?(M[w]=A[b],w++):_[b]===I?(M[w]=C[b],w++):E.splice(w,1)}return j[d]=E.length,S}}),Jt=["typed","DenseMatrix"],Wt=Object(s.a)("algorithm10",Jt,function(e){var M=e.typed,E=e.DenseMatrix;return function(e,t,r,n){var i=e._values,a=e._index,o=e._ptr,s=e._size,u=e._datatype;if(!i)throw new Error("Cannot perform operation on Pattern Sparse Matrix and Scalar value");var c,f=s[0],l=s[1],p=r;"string"==typeof u&&(c=u,t=M.convert(t,c),p=M.find(r,[c,c]));for(var m=[],h=new E({data:m,size:[f,l],datatype:c}),d=[],y=[],g=0;g<l;g++){for(var v=g+1,b=o[g],x=o[g+1],w=b;w<x;w++){var N=a[w];d[N]=i[w],y[N]=v}for(var O=0;O<f;O++)0===g&&(m[O]=[]),y[O]===v?m[O][g]=n?p(t,d[O]):p(d[O],t):m[O][g]=t}return h}}),Yt=["typed"],Xt=Object(s.a)("algorithm13",Yt,function(e){var h=e.typed;return function(e,t,r){var n,i=e._data,a=e._size,o=e._datatype,s=t._data,u=t._size,c=t._datatype,f=[];if(a.length!==u.length)throw new D.a(a.length,u.length);for(var l=0;l<a.length;l++){if(a[l]!==u[l])throw new RangeError("Dimension mismatch. Matrix A ("+a+") must match Matrix B ("+u+")");f[l]=a[l]}var p=r;"string"==typeof o&&o===c&&(n=o,p=h.find(r,[n,n]));var m=0<f.length?function e(t,r,n,i,a,o){var s=[];if(r===n.length-1)for(var u=0;u<i;u++)s[u]=t(a[u],o[u]);else for(var c=0;c<i;c++)s[c]=e(t,r+1,n,n[r+1],a[c],o[c]);return s}(p,0,f,f[0],i,s):[];return e.createDenseMatrix({data:m,size:f,datatype:n})}}),Qt=["typed"],Kt=Object(s.a)("algorithm14",Qt,function(e){var f=e.typed;return function(e,t,r,n){var i,a=e._data,o=e._size,s=e._datatype,u=r;"string"==typeof s&&(i=s,t=f.convert(t,i),u=f.find(r,[i,i]));var c=0<o.length?function e(t,r,n,i,a,o,s){var u=[];if(r===n.length-1)for(var c=0;c<i;c++)u[c]=s?t(o,a[c]):t(a[c],o);else for(var f=0;f<i;f++)u[f]=e(t,r+1,n,n[r+1],a[f],o,s);return u}(u,0,o,o[0],a,t,n):[];return e.createDenseMatrix({data:c,size:Object(ae.a)(o),datatype:i})}}),er=["typed","matrix","equalScalar","BigNumber","DenseMatrix"],tr=Object(s.a)("gcd",er,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.BigNumber,a=e.DenseMatrix,o=Gt({typed:t}),s=Vt({typed:t,equalScalar:n}),u=Wt({typed:t,DenseMatrix:a}),c=Xt({typed:t}),f=Kt({typed:t}),l=t("gcd",{"number, number":it,"BigNumber, BigNumber":function(e,t){if(!e.isInt()||!t.isInt())throw new Error("Parameters in function gcd must be integer numbers");var r=new i(0);for(;!t.isZero();){var n=e.mod(t);e=t,t=n}return e.lt(r)?e.neg():e},"Fraction, Fraction":function(e,t){return e.gcd(t)},"SparseMatrix, SparseMatrix":function(e,t){return s(e,t,l)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,l,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,l,!1)},"DenseMatrix, DenseMatrix":function(e,t){return c(e,t,l)},"Array, Array":function(e,t){return l(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return l(r(e),t)},"Matrix, Array":function(e,t){return l(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return u(e,t,l,!1)},"DenseMatrix, number | BigNumber":function(e,t){return f(e,t,l,!1)},"number | BigNumber, SparseMatrix":function(e,t){return u(t,e,l,!0)},"number | BigNumber, DenseMatrix":function(e,t){return f(t,e,l,!0)},"Array, number | BigNumber":function(e,t){return f(r(e),t,l,!1).valueOf()},"number | BigNumber, Array":function(e,t){return f(r(t),e,l,!0).valueOf()},"Array | Matrix | number | BigNumber, Array | Matrix | number | BigNumber, ...Array | Matrix | number | BigNumber":function(e,t,r){for(var n=l(e,t),i=0;i<r.length;i++)n=l(n,r[i]);return n}});return l}),rr=["typed","equalScalar"],nr=Object(s.a)("algorithm02",rr,function(e){var S=e.typed,A=e.equalScalar;return function(e,t,r,n){var i=e._data,a=e._size,o=e._datatype,s=t._values,u=t._index,c=t._ptr,f=t._size,l=t._datatype;if(a.length!==f.length)throw new D.a(a.length,f.length);if(a[0]!==f[0]||a[1]!==f[1])throw new RangeError("Dimension mismatch. Matrix A ("+a+") must match Matrix B ("+f+")");if(!s)throw new Error("Cannot perform operation on Dense Matrix and Pattern Sparse Matrix");var p,m=a[0],h=a[1],d=A,y=0,g=r;"string"==typeof o&&o===l&&(p=o,d=S.find(A,[p,p]),y=S.convert(0,p),g=S.find(r,[p,p]));for(var v=[],b=[],x=[],w=0;w<h;w++){x[w]=b.length;for(var N=c[w],O=c[w+1],M=N;M<O;M++){var E=u[M],j=n?g(s[M],i[E][w]):g(i[E][w],s[M]);d(j,y)||(b.push(E),v.push(j))}}return x[h]=b.length,t.createSparseMatrix({values:v,index:b,ptr:x,size:[m,h],datatype:p})}}),ir=["typed","equalScalar"],ar=Object(s.a)("algorithm06",ir,function(e){var A=e.typed,C=e.equalScalar;return function(e,t,r){var n=e._values,i=e._size,a=e._datatype,o=t._values,s=t._size,u=t._datatype;if(i.length!==s.length)throw new D.a(i.length,s.length);if(i[0]!==s[0]||i[1]!==s[1])throw new RangeError("Dimension mismatch. Matrix A ("+i+") must match Matrix B ("+s+")");var c,f=i[0],l=i[1],p=C,m=0,h=r;"string"==typeof a&&a===u&&(c=a,p=A.find(C,[c,c]),m=A.convert(0,c),h=A.find(r,[c,c]));for(var d=n&&o?[]:void 0,y=[],g=[],v=e.createSparseMatrix({values:d,index:y,ptr:g,size:[f,l],datatype:c}),b=d?[]:void 0,x=[],w=[],N=0;N<l;N++){g[N]=y.length;var O=N+1;if(H(e,N,x,b,w,O,v,h),H(t,N,x,b,w,O,v,h),b)for(var M=g[N];M<y.length;){var E=y[M];if(w[E]===O){var j=b[E];p(j,m)?y.splice(M,1):(d.push(j),M++)}else y.splice(M,1)}else for(var S=g[N];S<y.length;){w[y[S]]!==O?y.splice(S,1):S++}}return g[l]=y.length,v}}),or=["typed","equalScalar"],sr=Object(s.a)("algorithm11",or,function(e){var E=e.typed,j=e.equalScalar;return function(e,t,r,n){var i=e._values,a=e._index,o=e._ptr,s=e._size,u=e._datatype;if(!i)throw new Error("Cannot perform operation on Pattern Sparse Matrix and Scalar value");var c,f=s[0],l=s[1],p=j,m=0,h=r;"string"==typeof u&&(c=u,p=E.find(j,[c,c]),m=E.convert(0,c),t=E.convert(t,c),h=E.find(r,[c,c]));for(var d=[],y=[],g=[],v=e.createSparseMatrix({values:d,index:y,ptr:g,size:[f,l],datatype:c}),b=0;b<l;b++){g[b]=y.length;for(var x=o[b],w=o[b+1],N=x;N<w;N++){var O=a[N],M=n?h(t,i[N]):h(i[N],t);p(M,m)||(y.push(O),d.push(M))}}return g[l]=y.length,v}}),ur=["typed","matrix","equalScalar"],cr=Object(s.a)("lcm",ur,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=nr({typed:t,equalScalar:n}),a=ar({typed:t,equalScalar:n}),o=sr({typed:t,equalScalar:n}),s=Xt({typed:t}),u=Kt({typed:t}),c=t("lcm",{"number, number":at,"BigNumber, BigNumber":function(e,t){if(!e.isInt()||!t.isInt())throw new Error("Parameters in function lcm must be integer numbers");if(e.isZero())return e;if(t.isZero())return t;var r=e.times(t);for(;!t.isZero();){var n=t;t=e.mod(n),e=n}return r.div(e).abs()},"Fraction, Fraction":function(e,t){return e.lcm(t)},"SparseMatrix, SparseMatrix":function(e,t){return a(e,t,c)},"SparseMatrix, DenseMatrix":function(e,t){return i(t,e,c,!0)},"DenseMatrix, SparseMatrix":function(e,t){return i(e,t,c,!1)},"DenseMatrix, DenseMatrix":function(e,t){return s(e,t,c)},"Array, Array":function(e,t){return c(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return c(r(e),t)},"Matrix, Array":function(e,t){return c(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return o(e,t,c,!1)},"DenseMatrix, number | BigNumber":function(e,t){return u(e,t,c,!1)},"number | BigNumber, SparseMatrix":function(e,t){return o(t,e,c,!0)},"number | BigNumber, DenseMatrix":function(e,t){return u(t,e,c,!0)},"Array, number | BigNumber":function(e,t){return u(r(e),t,c,!1).valueOf()},"number | BigNumber, Array":function(e,t){return u(r(t),e,c,!0).valueOf()},"Array | Matrix | number | BigNumber, Array | Matrix | number | BigNumber, ...Array | Matrix | number | BigNumber":function(e,t,r){for(var n=c(e,t),i=0;i<r.length;i++)n=c(n,r[i]);return n}});return c}),fr=["typed","config","Complex"],lr=Object(s.a)("log10",fr,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("log10",{number:function(e){return 0<=e||r.predictable?st(e):new n(e,0).log().div(Math.LN10)},Complex:function(e){return new n(e).log().div(Math.LN10)},BigNumber:function(e){return!e.isNegative()||r.predictable?e.log():new n(e.toNumber(),0).log().div(Math.LN10)},"Array | Matrix":function(e){return oe(e,i)}});return i}),pr=["typed","config","Complex"],mr=Object(s.a)("log2",pr,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("log2",{number:function(e){return 0<=e||r.predictable?ut(e):a(new n(e,0))},Complex:a,BigNumber:function(e){return!e.isNegative()||r.predictable?e.log(2):a(new n(e.toNumber(),0))},"Array | Matrix":function(e){return oe(e,i)}});function a(e){var t=Math.sqrt(e.re*e.re+e.im*e.im);return new n(Math.log2?Math.log2(t):Math.log(t)/Math.LN2,Math.atan2(e.im,e.re)/Math.LN2)}return i}),hr=["typed"],dr=Object(s.a)("algorithm03",hr,function(e){var A=e.typed;return function(e,t,r,n){var i=e._data,a=e._size,o=e._datatype,s=t._values,u=t._index,c=t._ptr,f=t._size,l=t._datatype;if(a.length!==f.length)throw new D.a(a.length,f.length);if(a[0]!==f[0]||a[1]!==f[1])throw new RangeError("Dimension mismatch. Matrix A ("+a+") must match Matrix B ("+f+")");if(!s)throw new Error("Cannot perform operation on Dense Matrix and Pattern Sparse Matrix");var p,m=a[0],h=a[1],d=0,y=r;"string"==typeof o&&o===l&&(p=o,d=A.convert(0,p),y=A.find(r,[p,p]));for(var g=[],v=0;v<m;v++)g[v]=[];for(var b=[],x=[],w=0;w<h;w++){for(var N=w+1,O=c[w],M=c[w+1],E=O;E<M;E++){var j=u[E];b[j]=n?y(s[E],i[j][w]):y(i[j][w],s[E]),x[j]=N}for(var S=0;S<m;S++)x[S]===N?g[S][w]=b[S]:g[S][w]=n?y(d,i[S][w]):y(i[S][w],d)}return e.createDenseMatrix({data:g,size:[m,h],datatype:p})}}),yr=["typed","equalScalar"],gr=Object(s.a)("algorithm05",yr,function(e){var k=e.typed,z=e.equalScalar;return function(e,t,r){var n=e._values,i=e._index,a=e._ptr,o=e._size,s=e._datatype,u=t._values,c=t._index,f=t._ptr,l=t._size,p=t._datatype;if(o.length!==l.length)throw new D.a(o.length,l.length);if(o[0]!==l[0]||o[1]!==l[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+l+")");var m,h=o[0],d=o[1],y=z,g=0,v=r;"string"==typeof s&&s===p&&(m=s,y=k.find(z,[m,m]),g=k.convert(0,m),v=k.find(r,[m,m]));var b,x,w,N,O=n&&u?[]:void 0,M=[],E=[],j=e.createSparseMatrix({values:O,index:M,ptr:E,size:[h,d],datatype:m}),S=O?[]:void 0,A=O?[]:void 0,C=[],T=[];for(x=0;x<d;x++){E[x]=M.length;var _=x+1;for(w=a[x],N=a[x+1];w<N;w++)b=i[w],M.push(b),C[b]=_,S&&(S[b]=n[w]);for(w=f[x],N=f[x+1];w<N;w++)C[b=c[w]]!==_&&M.push(b),T[b]=_,A&&(A[b]=u[w]);if(O)for(w=E[x];w<M.length;){var I=C[b=M[w]],q=T[b];if(I===_||q===_){var B=v(I===_?S[b]:g,q===_?A[b]:g);y(B,g)?M.splice(w,1):(O.push(B),w++)}}}return E[d]=M.length,j}}),vr=["typed","DenseMatrix"],br=Object(s.a)("algorithm12",vr,function(e){var M=e.typed,E=e.DenseMatrix;return function(e,t,r,n){var i=e._values,a=e._index,o=e._ptr,s=e._size,u=e._datatype;if(!i)throw new Error("Cannot perform operation on Pattern Sparse Matrix and Scalar value");var c,f=s[0],l=s[1],p=r;"string"==typeof u&&(c=u,t=M.convert(t,c),p=M.find(r,[c,c]));for(var m=[],h=new E({data:m,size:[f,l],datatype:c}),d=[],y=[],g=0;g<l;g++){for(var v=g+1,b=o[g],x=o[g+1],w=b;w<x;w++){var N=a[w];d[N]=i[w],y[N]=v}for(var O=0;O<f;O++)0===g&&(m[O]=[]),y[O]===v?m[O][g]=n?p(t,d[O]):p(d[O],t):m[O][g]=n?p(t,0):p(0,t)}return h}}),xr=["typed","matrix","equalScalar","DenseMatrix"],wr=Object(s.a)("mod",xr,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=nr({typed:t,equalScalar:n}),o=dr({typed:t}),s=gr({typed:t,equalScalar:n}),u=sr({typed:t,equalScalar:n}),c=br({typed:t,DenseMatrix:i}),f=Xt({typed:t}),l=Kt({typed:t}),p=t("mod",{"number, number":ct,"BigNumber, BigNumber":function(e,t){return t.isZero()?e:e.mod(t)},"Fraction, Fraction":function(e,t){return e.mod(t)},"SparseMatrix, SparseMatrix":function(e,t){return s(e,t,p,!1)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,p,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,p,!1)},"DenseMatrix, DenseMatrix":function(e,t){return f(e,t,p)},"Array, Array":function(e,t){return p(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return p(r(e),t)},"Matrix, Array":function(e,t){return p(e,r(t))},"SparseMatrix, any":function(e,t){return u(e,t,p,!1)},"DenseMatrix, any":function(e,t){return l(e,t,p,!1)},"any, SparseMatrix":function(e,t){return c(t,e,p,!0)},"any, DenseMatrix":function(e,t){return l(t,e,p,!0)},"Array, any":function(e,t){return l(r(e),t,p,!1).valueOf()},"any, Array":function(e,t){return l(r(t),e,p,!0).valueOf()}});return p}),Nr=["typed"],Or=Object(s.a)("multiplyScalar",Nr,function(e){var n=(0,e.typed)("multiplyScalar",{"number, number":Ye,"Complex, Complex":function(e,t){return e.mul(t)},"BigNumber, BigNumber":function(e,t){return e.times(t)},"Fraction, Fraction":function(e,t){return e.mul(t)},"number | Fraction | BigNumber | Complex, Unit":function(e,t){var r=t.clone();return r.value=null===r.value?r._normalize(e):n(r.value,e),r},"Unit, number | Fraction | BigNumber | Complex":function(e,t){var r=e.clone();return r.value=null===r.value?r._normalize(t):n(r.value,t),r},"Unit, Unit":function(e,t){return e.multiply(t)}});return n}),Mr="multiply",Er=["typed","matrix","addScalar","multiplyScalar","equalScalar"],jr=Object(s.a)(Mr,Er,function(e){var z=e.typed,n=e.matrix,D=e.addScalar,R=e.multiplyScalar,B=e.equalScalar,r=sr({typed:z,equalScalar:B}),i=Kt({typed:z}),a=z(Mr,Object(ae.e)({"Array, Array":function(e,t){o(Object(I.a)(e),Object(I.a)(t));var r=a(n(e),n(t));return Object(ie.v)(r)?r.valueOf():r},"Matrix, Matrix":function(e,t){var r=e.size(),n=t.size();return o(r,n),1===r.length?1===n.length?function(e,t,r){if(0===r)throw new Error("Cannot multiply two empty vectors");var n,i=e._data,a=e._datatype,o=t._data,s=t._datatype,u=D,c=R;a&&s&&a===s&&"string"==typeof a&&(n=a,u=z.find(D,[n,n]),c=z.find(R,[n,n]));for(var f=c(i[0],o[0]),l=1;l<r;l++)f=u(f,c(i[l],o[l]));return f}(e,t,r[0]):function(e,t){if("dense"===t.storage())return function(e,t){var r,n=e._data,i=e._size,a=e._datatype,o=t._data,s=t._size,u=t._datatype,c=i[0],f=s[1],l=D,p=R;a&&u&&a===u&&"string"==typeof a&&(r=a,l=z.find(D,[r,r]),p=z.find(R,[r,r]));for(var m=[],h=0;h<f;h++){for(var d=p(n[0],o[0][h]),y=1;y<c;y++)d=l(d,p(n[y],o[y][h]));m[h]=d}return e.createDenseMatrix({data:m,size:[f],datatype:r})}(e,t);throw new Error("Support for SparseMatrix not implemented")}(e,t):1===n.length?s(e,t):u(e,t)},"Matrix, Array":function(e,t){return a(e,n(t))},"Array, Matrix":function(e,t){return a(n(e,t.storage()),t)},"SparseMatrix, any":function(e,t){return r(e,t,R,!1)},"DenseMatrix, any":function(e,t){return i(e,t,R,!1)},"any, SparseMatrix":function(e,t){return r(t,e,R,!0)},"any, DenseMatrix":function(e,t){return i(t,e,R,!0)},"Array, any":function(e,t){return i(n(e),t,R,!1).valueOf()},"any, Array":function(e,t){return i(n(t),e,R,!0).valueOf()},"any, any":R,"any, any, ...any":function(e,t,r){for(var n=a(e,t),i=0;i<r.length;i++)n=a(n,r[i]);return n}},R.signatures));function o(e,t){switch(e.length){case 1:switch(t.length){case 1:if(e[0]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Vectors must have the same length");break;case 2:if(e[0]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Vector length ("+e[0]+") must match Matrix rows ("+t[0]+")");break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix B has "+t.length+" dimensions)")}break;case 2:switch(t.length){case 1:if(e[1]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Matrix columns ("+e[1]+") must match Vector length ("+t[0]+")");break;case 2:if(e[1]!==t[0])throw new RangeError("Dimension mismatch in multiplication. Matrix A columns ("+e[1]+") must match Matrix B rows ("+t[0]+")");break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix B has "+t.length+" dimensions)")}break;default:throw new Error("Can only multiply a 1 or 2 dimensional matrix (Matrix A has "+e.length+" dimensions)")}}var s=z("_multiplyMatrixVector",{"DenseMatrix, any":function(e,t){var r,n=e._data,i=e._size,a=e._datatype,o=t._data,s=t._datatype,u=i[0],c=i[1],f=D,l=R;a&&s&&a===s&&"string"==typeof a&&(r=a,f=z.find(D,[r,r]),l=z.find(R,[r,r]));for(var p=[],m=0;m<u;m++){for(var h=n[m],d=l(h[0],o[0]),y=1;y<c;y++)d=f(d,l(h[y],o[y]));p[m]=d}return e.createDenseMatrix({data:p,size:[u],datatype:r})},"SparseMatrix, any":function(e,t){var r=e._values,n=e._index,i=e._ptr,a=e._datatype;if(!r)throw new Error("Cannot multiply Pattern only Matrix times Dense Matrix");var o,s=t._data,u=t._datatype,c=e._size[0],f=t._size[0],l=[],p=[],m=[],h=D,d=R,y=B,g=0;a&&u&&a===u&&"string"==typeof a&&(o=a,h=z.find(D,[o,o]),d=z.find(R,[o,o]),y=z.find(B,[o,o]),g=z.convert(0,o));for(var v=[],b=[],x=m[0]=0;x<f;x++){var w=s[x];if(!y(w,g))for(var N=i[x],O=i[x+1],M=N;M<O;M++){var E=n[M];b[E]?v[E]=h(v[E],d(w,r[M])):(b[E]=!0,p.push(E),v[E]=d(w,r[M]))}}for(var j=p.length,S=0;S<j;S++){var A=p[S];l[S]=v[A]}return m[1]=p.length,e.createSparseMatrix({values:l,index:p,ptr:m,size:[c,1],datatype:o})}}),u=z("_multiplyMatrixMatrix",{"DenseMatrix, DenseMatrix":function(e,t){var r,n=e._data,i=e._size,a=e._datatype,o=t._data,s=t._size,u=t._datatype,c=i[0],f=i[1],l=s[1],p=D,m=R;a&&u&&a===u&&"string"==typeof a&&(r=a,p=z.find(D,[r,r]),m=z.find(R,[r,r]));for(var h=[],d=0;d<c;d++){var y=n[d];h[d]=[];for(var g=0;g<l;g++){for(var v=m(y[0],o[0][g]),b=1;b<f;b++)v=p(v,m(y[b],o[b][g]));h[d][g]=v}}return e.createDenseMatrix({data:h,size:[c,l],datatype:r})},"DenseMatrix, SparseMatrix":function(e,t){var r=e._data,n=e._size,i=e._datatype,a=t._values,o=t._index,s=t._ptr,u=t._size,c=t._datatype;if(!a)throw new Error("Cannot multiply Dense Matrix times Pattern only Matrix");var f,l=n[0],p=u[1],m=D,h=R,d=B,y=0;i&&c&&i===c&&"string"==typeof i&&(f=i,m=z.find(D,[f,f]),h=z.find(R,[f,f]),d=z.find(B,[f,f]),y=z.convert(0,f));for(var g=[],v=[],b=[],x=t.createSparseMatrix({values:g,index:v,ptr:b,size:[l,p],datatype:f}),w=0;w<p;w++){b[w]=v.length;var N=s[w],O=s[w+1];if(N<O)for(var M=0,E=0;E<l;E++){for(var j=E+1,S=void 0,A=N;A<O;A++){var C=o[A];M!==j?(S=h(r[E][C],a[A]),M=j):S=m(S,h(r[E][C],a[A]))}M!==j||d(S,y)||(v.push(E),g.push(S))}}return b[p]=v.length,x},"SparseMatrix, DenseMatrix":function(e,t){var r=e._values,n=e._index,i=e._ptr,a=e._datatype;if(!r)throw new Error("Cannot multiply Pattern only Matrix times Dense Matrix");var o,s=t._data,u=t._datatype,c=e._size[0],f=t._size[0],l=t._size[1],p=D,m=R,h=B,d=0;a&&u&&a===u&&"string"==typeof a&&(o=a,p=z.find(D,[o,o]),m=z.find(R,[o,o]),h=z.find(B,[o,o]),d=z.convert(0,o));for(var y=[],g=[],v=[],b=e.createSparseMatrix({values:y,index:g,ptr:v,size:[c,l],datatype:o}),x=[],w=[],N=0;N<l;N++){v[N]=g.length;for(var O=N+1,M=0;M<f;M++){var E=s[M][N];if(!h(E,d))for(var j=i[M],S=i[M+1],A=j;A<S;A++){var C=n[A];w[C]!==O?(w[C]=O,g.push(C),x[C]=m(E,r[A])):x[C]=p(x[C],m(E,r[A]))}}for(var T=v[N],_=g.length,I=T;I<_;I++){var q=g[I];y[I]=x[q]}}return v[l]=g.length,b},"SparseMatrix, SparseMatrix":function(e,t){var r,n=e._values,i=e._index,a=e._ptr,o=e._datatype,s=t._values,u=t._index,c=t._ptr,f=t._datatype,l=e._size[0],p=t._size[1],m=n&&s,h=D,d=R;o&&f&&o===f&&"string"==typeof o&&(r=o,h=z.find(D,[r,r]),d=z.find(R,[r,r]));for(var y,g,v,b,x,w,N,O,M=m?[]:void 0,E=[],j=[],S=e.createSparseMatrix({values:M,index:E,ptr:j,size:[l,p],datatype:r}),A=m?[]:void 0,C=[],T=0;T<p;T++){j[T]=E.length;var _=T+1;for(x=c[T],w=c[T+1],b=x;b<w;b++)if(O=u[b],m)for(g=a[O],v=a[O+1],y=g;y<v;y++)N=i[y],C[N]!==_?(C[N]=_,E.push(N),A[N]=d(s[b],n[y])):A[N]=h(A[N],d(s[b],n[y]));else for(g=a[O],v=a[O+1],y=g;y<v;y++)N=i[y],C[N]!==_&&(C[N]=_,E.push(N));if(m)for(var I=j[T],q=E.length,B=I;B<q;B++){var k=E[B];M[B]=A[k]}}return j[p]=E.length,S}});return a}),Sr="nthRoot",Ar=["typed","matrix","equalScalar","BigNumber"],Cr=Object(s.a)(Sr,Ar,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,u=e.BigNumber,i=Gt({typed:t}),a=nr({typed:t,equalScalar:n}),o=ar({typed:t,equalScalar:n}),s=sr({typed:t,equalScalar:n}),c=Xt({typed:t}),f=Kt({typed:t}),l="Complex number not supported in function nthRoot. Use nthRoots instead.",p=t(Sr,{number:function(e){return ft(e,2)},"number, number":ft,BigNumber:function(e){return m(e,new u(2))},Complex:function(e){throw new Error(l)},"Complex, number":function(e,t){throw new Error(l)},"BigNumber, BigNumber":m,"Array | Matrix":function(e){return p(e,2)},"SparseMatrix, SparseMatrix":function(e,t){if(1===t.density())return o(e,t,p);throw new Error("Root must be non-zero")},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,p,!0)},"DenseMatrix, SparseMatrix":function(e,t){if(1===t.density())return i(e,t,p,!1);throw new Error("Root must be non-zero")},"DenseMatrix, DenseMatrix":function(e,t){return c(e,t,p)},"Array, Array":function(e,t){return p(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return p(r(e),t)},"Matrix, Array":function(e,t){return p(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return s(e,t,p,!1)},"DenseMatrix, number | BigNumber":function(e,t){return f(e,t,p,!1)},"number | BigNumber, SparseMatrix":function(e,t){if(1===t.density())return s(t,e,p,!0);throw new Error("Root must be non-zero")},"number | BigNumber, DenseMatrix":function(e,t){return f(t,e,p,!0)},"Array, number | BigNumber":function(e,t){return p(r(e),t).valueOf()},"number | BigNumber, Array":function(e,t){return p(e,r(t)).valueOf()}});return p;function m(e,t){var r=u.precision,n=u.clone({precision:r+2}),i=new u(0),a=new n(1),o=t.isNegative();if(o&&(t=t.neg()),t.isZero())throw new Error("Root must be non-zero");if(e.isNegative()&&!t.abs().mod(2).equals(1))throw new Error("Root must be odd when a is negative.");if(e.isZero())return o?new n(1/0):0;if(!e.isFinite())return o?i:e;var s=e.abs().pow(a.div(t));return s=e.isNeg()?s.neg():s,new u((o?a.div(s):s).toPrecision(r))}}),Tr=["typed","BigNumber","Fraction"],_r=Object(s.a)("sign",Tr,function(e){var t=e.typed,r=e.BigNumber,n=e.Fraction,i=t("sign",{number:lt,Complex:function(e){return e.sign()},BigNumber:function(e){return new r(e.cmp(0))},Fraction:function(e){return new n(e.s,1)},"Array | Matrix":function(e){return oe(e,i,!0)},Unit:function(e){return i(e.value)}});return i}),Ir=["config","typed","Complex"],qr=Object(s.a)("sqrt",Ir,function(e){var t=e.config,r=e.typed,n=e.Complex,i=r("sqrt",{number:a,Complex:function(e){return e.sqrt()},BigNumber:function(e){return!e.isNegative()||t.predictable?e.sqrt():a(e.toNumber())},"Array | Matrix":function(e){return oe(e,i,!0)},Unit:function(e){return e.pow(.5)}});function a(e){return isNaN(e)?NaN:0<=e||t.predictable?Math.sqrt(e):new n(e,0).sqrt()}return i}),Br=["typed"],kr=Object(s.a)("square",Br,function(e){var t=(0,e.typed)("square",{number:pt,Complex:function(e){return e.mul(e)},BigNumber:function(e){return e.times(e)},Fraction:function(e){return e.mul(e)},"Array | Matrix":function(e){return oe(e,t,!0)},Unit:function(e){return e.pow(2)}});return t}),zr="subtract",Dr=["typed","matrix","equalScalar","addScalar","unaryMinus","DenseMatrix"],Rr=Object(s.a)(zr,Dr,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.addScalar,a=e.unaryMinus,o=e.DenseMatrix,s=Gt({typed:t}),u=dr({typed:t}),c=gr({typed:t,equalScalar:n}),f=Wt({typed:t,DenseMatrix:o}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(zr,{"number, number":function(e,t){return e-t},"Complex, Complex":function(e,t){return e.sub(t)},"BigNumber, BigNumber":function(e,t){return e.minus(t)},"Fraction, Fraction":function(e,t){return e.sub(t)},"Unit, Unit":function(e,t){if(null===e.value)throw new Error("Parameter x contains a unit with undefined value");if(null===t.value)throw new Error("Parameter y contains a unit with undefined value");if(!e.equalBase(t))throw new Error("Units do not match");var r=e.clone();return r.value=m(r.value,t.value),r.fixPrefix=!1,r},"SparseMatrix, SparseMatrix":function(e,t){return Pr(e,t),c(e,t,m)},"SparseMatrix, DenseMatrix":function(e,t){return Pr(e,t),u(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return Pr(e,t),s(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return Pr(e,t),l(e,t,m)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, any":function(e,t){return f(e,a(t),i)},"DenseMatrix, any":function(e,t){return p(e,t,m)},"any, SparseMatrix":function(e,t){return f(t,e,m,!0)},"any, DenseMatrix":function(e,t){return p(t,e,m,!0)},"Array, any":function(e,t){return p(r(e),t,m,!1).valueOf()},"any, Array":function(e,t){return p(r(t),e,m,!0).valueOf()}});return m});function Pr(e,t){var r=e.size(),n=t.size();if(r.length!==n.length)throw new D.a(r.length,n.length)}var Fr=["typed","config","matrix","BigNumber"],Ur=Object(s.a)("xgcd",Fr,function(e){var t=e.typed,p=e.config,m=e.matrix,h=e.BigNumber;return t("xgcd",{"number, number":function(e,t){var r=mt(e,t);return"Array"===p.matrix?r:m(r)},"BigNumber, BigNumber":function(e,t){var r,n,i,a,o=new h(0),s=new h(1),u=o,c=s,f=s,l=o;if(!e.isInt()||!t.isInt())throw new Error("Parameters in function xgcd must be integer numbers");for(;!t.isZero();)n=e.div(t).floor(),i=e.mod(t),r=u,u=c.minus(n.times(u)),c=r,r=f,f=l.minus(n.times(f)),l=r,e=t,t=i;a=e.lt(o)?[e.neg(),c.neg(),l.neg()]:[e,e.isZero()?0:c,l];return"Array"===p.matrix?a:m(a)}})}),Lr=["typed","equalScalar"],Hr=Object(s.a)("algorithm09",Lr,function(e){var q=e.typed,B=e.equalScalar;return function(e,t,r){var n=e._values,i=e._index,a=e._ptr,o=e._size,s=e._datatype,u=t._values,c=t._index,f=t._ptr,l=t._size,p=t._datatype;if(o.length!==l.length)throw new D.a(o.length,l.length);if(o[0]!==l[0]||o[1]!==l[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+l+")");var m,h=o[0],d=o[1],y=B,g=0,v=r;"string"==typeof s&&s===p&&(m=s,y=q.find(B,[m,m]),g=q.convert(0,m),v=q.find(r,[m,m]));var b,x,w,N,O,M=n&&u?[]:void 0,E=[],j=[],S=e.createSparseMatrix({values:M,index:E,ptr:j,size:[h,d],datatype:m}),A=M?[]:void 0,C=[];for(x=0;x<d;x++){j[x]=E.length;var T=x+1;if(A)for(N=f[x],O=f[x+1],w=N;w<O;w++)C[b=c[w]]=T,A[b]=u[w];for(N=a[x],O=a[x+1],w=N;w<O;w++)if(b=i[w],A){var _=C[b]===T?A[b]:g,I=v(n[w],_);y(I,g)||(E.push(b),M.push(I))}else E.push(b)}return j[d]=E.length,S}}),$r="dotMultiply",Gr=["typed","matrix","equalScalar","multiplyScalar"],Zr=Object(s.a)($r,Gr,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.multiplyScalar,a=nr({typed:t,equalScalar:n}),o=Hr({typed:t,equalScalar:n}),s=sr({typed:t,equalScalar:n}),u=Xt({typed:t}),c=Kt({typed:t}),f=t($r,{"any, any":i,"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,i,!1)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,i,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,i,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,i)},"Array, Array":function(e,t){return f(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return f(r(e),t)},"Matrix, Array":function(e,t){return f(e,r(t))},"SparseMatrix, any":function(e,t){return s(e,t,i,!1)},"DenseMatrix, any":function(e,t){return c(e,t,i,!1)},"any, SparseMatrix":function(e,t){return s(t,e,i,!0)},"any, DenseMatrix":function(e,t){return c(t,e,i,!0)},"Array, any":function(e,t){return c(r(e),t,i,!1).valueOf()},"any, Array":function(e,t){return c(r(t),e,i,!0).valueOf()}});return f});function Vr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function bitAnd");var r=e.constructor;if(e.isNaN()||t.isNaN())return new r(NaN);if(e.isZero()||t.eq(-1)||e.eq(t))return e;if(t.isZero()||e.eq(-1))return t;if(!e.isFinite()||!t.isFinite()){if(!e.isFinite()&&!t.isFinite())return e.isNegative()===t.isNegative()?e:new r(0);if(!e.isFinite())return t.isNegative()?e:e.isNegative()?new r(0):t;if(!t.isFinite())return e.isNegative()?t:t.isNegative()?new r(0):e}return Yr(e,t,function(e,t){return e&t})}function Jr(e){if(e.isFinite()&&!e.isInteger())throw new Error("Integer expected in function bitNot");var t=e.constructor,r=t.precision;t.config({precision:1e9});var n=e.plus(new t(1));return n.s=-n.s||null,t.config({precision:r}),n}function Wr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function bitOr");var r=e.constructor;if(e.isNaN()||t.isNaN())return new r(NaN);var n=new r(-1);return e.isZero()||t.eq(n)||e.eq(t)?t:t.isZero()||e.eq(n)?e:e.isFinite()&&t.isFinite()?Yr(e,t,function(e,t){return e|t}):!e.isFinite()&&!e.isNegative()&&t.isNegative()||e.isNegative()&&!t.isNegative()&&!t.isFinite()?n:e.isNegative()&&t.isNegative()?e.isFinite()?e:t:e.isFinite()?t:e}function Yr(e,t,r){var n,i,a,o,s,u=e.constructor,c=+(e.s<0),f=+(t.s<0);if(c){n=Xr(Jr(e));for(var l=0;l<n.length;++l)n[l]^=1}else n=Xr(e);if(f){i=Xr(Jr(t));for(var p=0;p<i.length;++p)i[p]^=1}else i=Xr(t);s=n.length<=i.length?(a=n,o=i,c):(a=i,o=n,f);var m=a.length,h=o.length,d=1^r(c,f),y=new u(1^d),g=new u(1),v=new u(2),b=u.precision;for(u.config({precision:1e9});0<m;)r(a[--m],o[--h])===d&&(y=y.plus(g)),g=g.times(v);for(;0<h;)r(s,o[--h])===d&&(y=y.plus(g)),g=g.times(v);return u.config({precision:b}),0==d&&(y.s=-y.s),y}function Xr(e){for(var t=e.d,r=t[0]+"",n=1;n<t.length;++n){for(var i=t[n]+"",a=7-i.length;a--;)i="0"+i;r+=i}for(var o=r.length;"0"===r.charAt(o);)o--;var s=e.e,u=r.slice(0,o+1||1),c=u.length;if(0<s)if(++s>c)for(s-=c;s--;)u+="0";else s<c&&(u=u.slice(0,s)+"."+u.slice(s));for(var f=[0],l=0;l<u.length;){for(var p=f.length;p--;)f[p]*=10;f[0]+=parseInt(u.charAt(l++));for(var m=0;m<f.length;++m)1<f[m]&&(null!==f[m+1]&&void 0!==f[m+1]||(f[m+1]=0),f[m+1]+=f[m]>>1,f[m]&=1)}return f.reverse()}function Qr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function bitXor");var r=e.constructor;if(e.isNaN()||t.isNaN())return new r(NaN);if(e.isZero())return t;if(t.isZero())return e;if(e.eq(t))return new r(0);var n=new r(-1);return e.eq(n)?Jr(t):t.eq(n)?Jr(e):e.isFinite()&&t.isFinite()?Yr(e,t,function(e,t){return e^t}):e.isFinite()||t.isFinite()?new r(e.isNegative()===t.isNegative()?1/0:-1/0):n}function Kr(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function leftShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:e.isFinite()||t.isFinite()?t.lt(55)?e.times(Math.pow(2,t.toNumber())+""):e.times(new r(2).pow(t)):new r(NaN)}function en(e,t){if(e.isFinite()&&!e.isInteger()||t.isFinite()&&!t.isInteger())throw new Error("Integers expected in function rightArithShift");var r=e.constructor;return e.isNaN()||t.isNaN()||t.isNegative()&&!t.isZero()?new r(NaN):e.isZero()||t.isZero()?e:t.isFinite()?t.lt(55)?e.div(Math.pow(2,t.toNumber())+"").floor():e.div(new r(2).pow(t)).floor():e.isNegative()?new r(-1):e.isFinite()?new r(0):new r(NaN)}var tn="number, number";function rn(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function bitAnd");return e&t}function nn(e){if(!Object(j.i)(e))throw new Error("Integer expected in function bitNot");return~e}function an(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function bitOr");return e|t}function on(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function bitXor");return e^t}function sn(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function leftShift");return e<<t}function un(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function rightArithShift");return e>>t}function cn(e,t){if(!Object(j.i)(e)||!Object(j.i)(t))throw new Error("Integers expected in function rightLogShift");return e>>>t}rn.signature=tn,nn.signature="number",cn.signature=un.signature=sn.signature=on.signature=an.signature=tn;var fn=["typed","matrix","equalScalar"],ln=Object(s.a)("bitAnd",fn,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=nr({typed:t,equalScalar:n}),a=ar({typed:t,equalScalar:n}),o=sr({typed:t,equalScalar:n}),s=Xt({typed:t}),u=Kt({typed:t}),c=t("bitAnd",{"number, number":rn,"BigNumber, BigNumber":Vr,"SparseMatrix, SparseMatrix":function(e,t){return a(e,t,c,!1)},"SparseMatrix, DenseMatrix":function(e,t){return i(t,e,c,!0)},"DenseMatrix, SparseMatrix":function(e,t){return i(e,t,c,!1)},"DenseMatrix, DenseMatrix":function(e,t){return s(e,t,c)},"Array, Array":function(e,t){return c(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return c(r(e),t)},"Matrix, Array":function(e,t){return c(e,r(t))},"SparseMatrix, any":function(e,t){return o(e,t,c,!1)},"DenseMatrix, any":function(e,t){return u(e,t,c,!1)},"any, SparseMatrix":function(e,t){return o(t,e,c,!0)},"any, DenseMatrix":function(e,t){return u(t,e,c,!0)},"Array, any":function(e,t){return u(r(e),t,c,!1).valueOf()},"any, Array":function(e,t){return u(r(t),e,c,!0).valueOf()}});return c}),pn=["typed"],mn=Object(s.a)("bitNot",pn,function(e){var t=(0,e.typed)("bitNot",{number:nn,BigNumber:Jr,"Array | Matrix":function(e){return oe(e,t)}});return t}),hn=["typed","matrix","equalScalar","DenseMatrix"],dn=Object(s.a)("bitOr",hn,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=Gt({typed:t}),o=Vt({typed:t,equalScalar:n}),s=Wt({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t("bitOr",{"number, number":an,"BigNumber, BigNumber":Wr,"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return f(r(e),t)},"Matrix, Array":function(e,t){return f(e,r(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(r(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(r(t),e,f,!0).valueOf()}});return f}),yn=["typed","DenseMatrix"],gn=Object(s.a)("algorithm07",yn,function(e){var O=e.typed,M=e.DenseMatrix;return function(e,t,r){var n=e._size,i=e._datatype,a=t._size,o=t._datatype;if(n.length!==a.length)throw new D.a(n.length,a.length);if(n[0]!==a[0]||n[1]!==a[1])throw new RangeError("Dimension mismatch. Matrix A ("+n+") must match Matrix B ("+a+")");var s,u,c,f=n[0],l=n[1],p=0,m=r;"string"==typeof i&&i===o&&(s=i,p=O.convert(0,s),m=O.find(r,[s,s]));var h=[];for(u=0;u<f;u++)h[u]=[];var d=new M({data:h,size:[f,l],datatype:s}),y=[],g=[],v=[],b=[];for(c=0;c<l;c++){var x=c+1;for(E(e,c,v,y,x),E(t,c,b,g,x),u=0;u<f;u++){var w=v[u]===x?y[u]:p,N=b[u]===x?g[u]:p;h[u][c]=m(w,N)}}return d};function E(e,t,r,n,i){for(var a=e._values,o=e._index,s=e._ptr,u=s[t],c=s[t+1];u<c;u++){var f=o[u];r[f]=i,n[f]=a[u]}}}),vn=["typed","matrix","DenseMatrix"],bn=Object(s.a)("bitXor",vn,function(e){var t=e.typed,r=e.matrix,n=e.DenseMatrix,i=dr({typed:t}),a=gn({typed:t,DenseMatrix:n}),o=br({typed:t,DenseMatrix:n}),s=Xt({typed:t}),u=Kt({typed:t}),c=t("bitXor",{"number, number":on,"BigNumber, BigNumber":Qr,"SparseMatrix, SparseMatrix":function(e,t){return a(e,t,c)},"SparseMatrix, DenseMatrix":function(e,t){return i(t,e,c,!0)},"DenseMatrix, SparseMatrix":function(e,t){return i(e,t,c,!1)},"DenseMatrix, DenseMatrix":function(e,t){return s(e,t,c)},"Array, Array":function(e,t){return c(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return c(r(e),t)},"Matrix, Array":function(e,t){return c(e,r(t))},"SparseMatrix, any":function(e,t){return o(e,t,c,!1)},"DenseMatrix, any":function(e,t){return u(e,t,c,!1)},"any, SparseMatrix":function(e,t){return o(t,e,c,!0)},"any, DenseMatrix":function(e,t){return u(t,e,c,!0)},"Array, any":function(e,t){return u(r(e),t,c,!1).valueOf()},"any, Array":function(e,t){return u(r(t),e,c,!0).valueOf()}});return c}),xn=["typed"],wn=Object(s.a)("arg",xn,function(e){var t=(0,e.typed)("arg",{number:function(e){return Math.atan2(0,e)},BigNumber:function(e){return e.constructor.atan2(0,e)},Complex:function(e){return e.arg()},"Array | Matrix":function(e){return oe(e,t)}});return t}),Nn=["typed"],On=Object(s.a)("conj",Nn,function(e){var t=(0,e.typed)("conj",{number:function(e){return e},BigNumber:function(e){return e},Complex:function(e){return e.conjugate()},"Array | Matrix":function(e){return oe(e,t)}});return t}),Mn=["typed"],En=Object(s.a)("im",Mn,function(e){var t=(0,e.typed)("im",{number:function(e){return 0},BigNumber:function(e){return e.mul(0)},Complex:function(e){return e.im},"Array | Matrix":function(e){return oe(e,t)}});return t}),jn=["typed"],Sn=Object(s.a)("re",jn,function(e){var t=(0,e.typed)("re",{number:function(e){return e},BigNumber:function(e){return e},Complex:function(e){return e.re},"Array | Matrix":function(e){return oe(e,t)}});return t}),An="number, number";function Cn(e){return!e}function Tn(e,t){return!(!e&&!t)}function _n(e,t){return!!e!=!!t}function In(e,t){return!(!e||!t)}Cn.signature="number",In.signature=_n.signature=Tn.signature=An;var qn=["typed"],Bn=Object(s.a)("not",qn,function(e){var t=(0,e.typed)("not",{number:Cn,Complex:function(e){return 0===e.re&&0===e.im},BigNumber:function(e){return e.isZero()||e.isNaN()},Unit:function(e){return null===e.value||t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),kn=["typed","matrix","equalScalar","DenseMatrix"],zn=Object(s.a)("or",kn,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=dr({typed:t}),o=gr({typed:t,equalScalar:n}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t("or",{"number, number":Tn,"Complex, Complex":function(e,t){return 0!==e.re||0!==e.im||0!==t.re||0!==t.im},"BigNumber, BigNumber":function(e,t){return!e.isZero()&&!e.isNaN()||!t.isZero()&&!t.isNaN()},"Unit, Unit":function(e,t){return f(e.value||0,t.value||0)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return f(r(e),t)},"Matrix, Array":function(e,t){return f(e,r(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(r(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(r(t),e,f,!0).valueOf()}});return f}),Dn=["typed","matrix","DenseMatrix"],Rn=Object(s.a)("xor",Dn,function(e){var t=e.typed,r=e.matrix,n=e.DenseMatrix,i=dr({typed:t}),a=gn({typed:t,DenseMatrix:n}),o=br({typed:t,DenseMatrix:n}),s=Xt({typed:t}),u=Kt({typed:t}),c=t("xor",{"number, number":_n,"Complex, Complex":function(e,t){return(0!==e.re||0!==e.im)!=(0!==t.re||0!==t.im)},"BigNumber, BigNumber":function(e,t){return(!e.isZero()&&!e.isNaN())!=(!t.isZero()&&!t.isNaN())},"Unit, Unit":function(e,t){return c(e.value||0,t.value||0)},"SparseMatrix, SparseMatrix":function(e,t){return a(e,t,c)},"SparseMatrix, DenseMatrix":function(e,t){return i(t,e,c,!0)},"DenseMatrix, SparseMatrix":function(e,t){return i(e,t,c,!1)},"DenseMatrix, DenseMatrix":function(e,t){return s(e,t,c)},"Array, Array":function(e,t){return c(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return c(r(e),t)},"Matrix, Array":function(e,t){return c(e,r(t))},"SparseMatrix, any":function(e,t){return o(e,t,c,!1)},"DenseMatrix, any":function(e,t){return u(e,t,c,!1)},"any, SparseMatrix":function(e,t){return o(t,e,c,!0)},"any, DenseMatrix":function(e,t){return u(t,e,c,!0)},"Array, any":function(e,t){return u(r(e),t,c,!1).valueOf()},"any, Array":function(e,t){return u(r(t),e,c,!0).valueOf()}});return c}),Pn=["typed","matrix","isInteger"],Fn=Object(s.a)("concat",Pn,function(e){var t=e.typed,l=e.matrix,p=e.isInteger;return t("concat",{"...Array | Matrix | number | BigNumber":function(e){var t,r,n=e.length,i=-1,a=!1,o=[];for(t=0;t<n;t++){var s=e[t];if(Object(ie.v)(s)&&(a=!0),Object(ie.y)(s)||Object(ie.e)(s)){if(t!==n-1)throw new Error("Dimension must be specified as last argument");if(r=i,i=s.valueOf(),!p(i))throw new TypeError("Integer number expected for dimension");if(i<0||0<t&&r<i)throw new R.a(i,r+1)}else{var u=Object(ae.a)(s).valueOf(),c=Object(I.a)(u);if(o[t]=u,r=i,i=c.length-1,0<t&&i!==r)throw new D.a(r+1,i+1)}}if(0===o.length)throw new SyntaxError("At least one matrix expected");for(var f=o.shift();o.length;)f=Un(f,o.shift(),i,0);return a?l(f):f},"...string":function(e){return e.join("")}})});function Un(e,t,r,n){if(n<r){if(e.length!==t.length)throw new D.a(e.length,t.length);for(var i=[],a=0;a<e.length;a++)i[a]=Un(e[a],t[a],r,n+1);return i}return e.concat(t)}var Ln=["typed","Index","matrix","range"],Hn=Object(s.a)("column",Ln,function(e){var t=e.typed,i=e.Index,r=e.matrix,a=e.range;return t("column",{"Matrix, number":n,"Array, number":function(e,t){return n(r(Object(ae.a)(e)),t).valueOf()}});function n(e,t){if(2!==e.size().length)throw new Error("Only two dimensional matrix is supported");Object(I.s)(t,e.size()[1]);var r=a(0,e.size()[0]),n=new i(r,t);return e.subset(n)}}),$n=["typed","matrix","subtract","multiply"],Gn=Object(s.a)("cross",$n,function(e){var t=e.typed,r=e.matrix,o=e.subtract,s=e.multiply;return t("cross",{"Matrix, Matrix":function(e,t){return r(n(e.toArray(),t.toArray()))},"Matrix, Array":function(e,t){return r(n(e.toArray(),t))},"Array, Matrix":function(e,t){return r(n(e,t.toArray()))},"Array, Array":n});function n(e,t){var r=Math.max(Object(I.a)(e).length,Object(I.a)(t).length);e=Object(I.p)(e),t=Object(I.p)(t);var n=Object(I.a)(e),i=Object(I.a)(t);if(1!==n.length||1!==i.length||3!==n[0]||3!==i[0])throw new RangeError("Vectors with length 3 expected (Size A = ["+n.join(", ")+"], B = ["+i.join(", ")+"])");var a=[o(s(e[1],t[2]),s(e[2],t[1])),o(s(e[2],t[0]),s(e[0],t[2])),o(s(e[0],t[1]),s(e[1],t[0]))];return 1<r?[a]:a}}),Zn=["typed","matrix","DenseMatrix","SparseMatrix"],Vn=Object(s.a)("diag",Zn,function(e){var t=e.typed,f=e.matrix,u=e.DenseMatrix,c=e.SparseMatrix;return t("diag",{Array:function(e){return n(e,0,Object(I.a)(e),null)},"Array, number":function(e,t){return n(e,t,Object(I.a)(e),null)},"Array, BigNumber":function(e,t){return n(e,t.toNumber(),Object(I.a)(e),null)},"Array, string":function(e,t){return n(e,0,Object(I.a)(e),t)},"Array, number, string":function(e,t,r){return n(e,t,Object(I.a)(e),r)},"Array, BigNumber, string":function(e,t,r){return n(e,t.toNumber(),Object(I.a)(e),r)},Matrix:function(e){return n(e,0,e.size(),e.storage())},"Matrix, number":function(e,t){return n(e,t,e.size(),e.storage())},"Matrix, BigNumber":function(e,t){return n(e,t.toNumber(),e.size(),e.storage())},"Matrix, string":function(e,t){return n(e,0,e.size(),t)},"Matrix, number, string":function(e,t,r){return n(e,t,e.size(),r)},"Matrix, BigNumber, string":function(e,t,r){return n(e,t.toNumber(),e.size(),r)}});function n(e,t,r,n){if(!Object(j.i)(t))throw new TypeError("Second parameter in function diag must be an integer");var i=0<t?t:0,a=t<0?-t:0;switch(r.length){case 1:return function(e,t,r,n,i,a){var o=[n+i,n+a];if(r&&"sparse"!==r&&"dense"!==r)throw new TypeError("Unknown matrix type ".concat(r,'"'));var s="sparse"===r?c.diagonal(o,e,t):u.diagonal(o,e,t);return null!==r?s:s.valueOf()}(e,t,n,r[0],a,i);case 2:return function(e,t,r,n,i,a){if(Object(ie.v)(e)){var o=e.diagonal(t);return null!==r?r!==o.storage()?f(o,r):o:o.valueOf()}for(var s=Math.min(n[0]-i,n[1]-a),u=[],c=0;c<s;c++)u[c]=e[c+i][c+a];return null!==r?f(u):u}(e,t,n,r,a,i)}throw new RangeError("Matrix for function diag must be 2 dimensional")}}),Jn=Object(s.a)("eye",[],function(){return function(){throw new Error('Function "eye" is renamed to "identity" since mathjs version 5.0.0. To keep eye working, create an alias for it using "math.import({eye: math.identity}, {override: true})"')}});function Wn(e){return(Wn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Yn(i,a){return function e(){"object"!==Wn(e.cache)&&(e.cache={});for(var t=[],r=0;r<arguments.length;r++)t[r]=arguments[r];var n=a?a(t):JSON.stringify(t);return n in e.cache||(e.cache[n]=i.apply(i,t)),e.cache[n]}}function Xn(e){return Object.keys(e.signatures||{}).reduce(function(e,t){var r=(t.match(/,/g)||[]).length+1;return Math.max(e,r)},-1)}var Qn=["typed"],Kn=Object(s.a)("filter",Qn,function(e){return(0,e.typed)("filter",{"Array, function":ei,"Matrix, function":function(e,t){return e.create(ei(e.toArray(),t))},"Array, RegExp":I.d,"Matrix, RegExp":function(e,t){return e.create(Object(I.d)(e.toArray(),t))}})});function ei(e,n){var i=Xn(n);return Object(I.c)(e,function(e,t,r){return 1===i?n(e):2===i?n(e,[t]):n(e,[t],r)})}var ti="flatten",ri=["typed","matrix"],ni=Object(s.a)(ti,ri,function(e){var t=e.typed,r=e.matrix;return t(ti,{Array:function(e){return Object(I.e)(Object(ae.a)(e))},Matrix:function(e){var t=Object(I.e)(Object(ae.a)(e.toArray()));return r(t)}})}),ii="forEach",ai=["typed"],oi=Object(s.a)(ii,ai,function(e){return(0,e.typed)(ii,{"Array, function":si,"Matrix, function":function(e,t){return e.forEach(t)}})});function si(t,i){var a=Xn(i);!function r(e,n){Array.isArray(e)?Object(I.f)(e,function(e,t){r(e,n.concat(t))}):1===a?i(e):2===a?i(e,n):i(e,n,t)}(t,[])}var ui="getMatrixDataType",ci=["typed"],fi=Object(s.a)(ui,ci,function(e){return(0,e.typed)(ui,{Array:function(e){return Object(I.h)(e,ie.M)},Matrix:function(e){return e.getDataType()}})}),li="identity",pi=["typed","config","matrix","BigNumber","DenseMatrix","SparseMatrix"],mi=Object(s.a)(li,pi,function(e){var t=e.typed,r=e.config,n=e.matrix,f=e.BigNumber,l=e.DenseMatrix,p=e.SparseMatrix;return t(li,{"":function(){return"Matrix"===r.matrix?n([]):[]},string:function(e){return n(e)},"number | BigNumber":function(e){return a(e,e,"Matrix"===r.matrix?"dense":void 0)},"number | BigNumber, string":function(e,t){return a(e,e,t)},"number | BigNumber, number | BigNumber":function(e,t){return a(e,t,"Matrix"===r.matrix?"dense":void 0)},"number | BigNumber, number | BigNumber, string":function(e,t,r){return a(e,t,r)},Array:function(e){return i(e)},"Array, string":function(e,t){return i(e,t)},Matrix:function(e){return i(e.valueOf(),e.storage())},"Matrix, string":function(e,t){return i(e.valueOf(),t)}});function i(e,t){switch(e.length){case 0:return t?n(t):[];case 1:return a(e[0],e[0],t);case 2:return a(e[0],e[1],t);default:throw new Error("Vector containing two values expected")}}function a(e,t,r){var n=Object(ie.e)(e)||Object(ie.e)(t)?f:null;if(Object(ie.e)(e)&&(e=e.toNumber()),Object(ie.e)(t)&&(t=t.toNumber()),!Object(j.i)(e)||e<1)throw new Error("Parameters in function identity must be positive integers");if(!Object(j.i)(t)||t<1)throw new Error("Parameters in function identity must be positive integers");var i=n?new f(1):1,a=n?new n(0):0,o=[e,t];if(r){if("sparse"===r)return p.diagonal(o,i,0,a);if("dense"===r)return l.diagonal(o,i,0,a);throw new TypeError('Unknown matrix type "'.concat(r,'"'))}for(var s=Object(I.o)([],o,a),u=e<t?e:t,c=0;c<u;c++)s[c][c]=i;return s}}),hi=["typed","matrix","multiplyScalar"],di=Object(s.a)("kron",hi,function(e){var t=e.typed,r=e.matrix,a=e.multiplyScalar;return t("kron",{"Matrix, Matrix":function(e,t){return r(n(e.toArray(),t.toArray()))},"Matrix, Array":function(e,t){return r(n(e.toArray(),t))},"Array, Matrix":function(e,t){return r(n(e,t.toArray()))},"Array, Array":n});function n(e,r){if(1===Object(I.a)(e).length&&(e=[e]),1===Object(I.a)(r).length&&(r=[r]),2<Object(I.a)(e).length||2<Object(I.a)(r).length)throw new RangeError("Vectors with dimensions greater then 2 are not supported expected (Size x = "+JSON.stringify(e.length)+", y = "+JSON.stringify(r.length)+")");var n=[],i=[];return e.map(function(t){return r.map(function(e){return i=[],n.push(i),t.map(function(t){return e.map(function(e){return i.push(a(t,e))})})})})&&n}}),yi=["typed"],gi=Object(s.a)("map",yi,function(e){return(0,e.typed)("map",{"Array, function":vi,"Matrix, function":function(e,t){return e.map(t)}})});function vi(t,i){var a=Xn(i);return function r(e,n){return Array.isArray(e)?e.map(function(e,t){return r(e,n.concat(t))}):1===a?i(e):2===a?i(e,n):i(e,n,t)}(t,[])}var bi=["typed","config","matrix","BigNumber"],xi=Object(s.a)("ones",bi,function(e){var t=e.typed,r=e.config,a=e.matrix,o=e.BigNumber;return t("ones",{"":function(){return"Array"===r.matrix?n([]):n([],"default")},"...number | BigNumber | string":function(e){if("string"!=typeof e[e.length-1])return"Array"===r.matrix?n(e):n(e,"default");var t=e.pop();return n(e,t)},Array:n,Matrix:function(e){var t=e.storage();return n(e.valueOf(),t)},"Array | Matrix, string":function(e,t){return n(e.valueOf(),t)}});function n(e,t){var r=function(e){var n=!1;return e.forEach(function(e,t,r){Object(ie.e)(e)&&(n=!0,r[t]=e.toNumber())}),n}(e)?new o(1):1;if(function(e){e.forEach(function(e){if("number"!=typeof e||!Object(j.i)(e)||e<0)throw new Error("Parameters in function ones must be positive integers")})}(e),t){var n=a(t);return 0<e.length?n.resize(e,r):n}var i=[];return 0<e.length?Object(I.o)(i,e,r):i}});function wi(){throw new Error('No "bignumber" implementation available')}function Ni(){throw new Error('No "fraction" implementation available')}function Oi(){throw new Error('No "matrix" implementation available')}var Mi=["typed","config","?matrix","?bignumber","smaller","smallerEq","larger","largerEq"],Ei=Object(s.a)("range",Mi,function(e){var t=e.typed,n=e.config,r=e.matrix,o=e.bignumber,s=e.smaller,u=e.smallerEq,c=e.larger,f=e.largerEq;return t("range",{string:a,"string, boolean":a,"number, number":function(e,t){return i(l(e,t,1))},"number, number, number":function(e,t,r){return i(l(e,t,r))},"number, number, boolean":function(e,t,r){return i(r?p(e,t,1):l(e,t,1))},"number, number, number, boolean":function(e,t,r,n){return i(n?p(e,t,r):l(e,t,r))},"BigNumber, BigNumber":function(e,t){return i(m(e,t,new e.constructor(1)))},"BigNumber, BigNumber, BigNumber":function(e,t,r){return i(m(e,t,r))},"BigNumber, BigNumber, boolean":function(e,t,r){var n=e.constructor;return i(r?h(e,t,new n(1)):m(e,t,new n(1)))},"BigNumber, BigNumber, BigNumber, boolean":function(e,t,r,n){return i(n?h(e,t,r):m(e,t,r))}});function i(e){return"Matrix"===n.matrix?r?r(e):Oi():e}function a(e,t){var r=function(e){var t=e.split(":").map(function(e){return Number(e)});if(t.some(function(e){return isNaN(e)}))return null;switch(t.length){case 2:return{start:t[0],end:t[1],step:1};case 3:return{start:t[0],end:t[2],step:t[1]};default:return null}}(e);if(!r)throw new SyntaxError('String "'+e+'" is no valid range');return"BigNumber"===n.number?(void 0===o&&wi(),i((t?h:m)(o(r.start),o(r.end),o(r.step)))):i((t?p:l)(r.start,r.end,r.step))}function l(e,t,r){var n=[],i=e;if(0<r)for(;s(i,t);)n.push(i),i+=r;else if(r<0)for(;c(i,t);)n.push(i),i+=r;return n}function p(e,t,r){var n=[],i=e;if(0<r)for(;u(i,t);)n.push(i),i+=r;else if(r<0)for(;f(i,t);)n.push(i),i+=r;return n}function m(e,t,r){var n=o(0),i=[],a=e;if(r.gt(n))for(;s(a,t);)i.push(a),a=a.plus(r);else if(r.lt(n))for(;c(a,t);)i.push(a),a=a.plus(r);return i}function h(e,t,r){var n=o(0),i=[],a=e;if(r.gt(n))for(;u(a,t);)i.push(a),a=a.plus(r);else if(r.lt(n))for(;f(a,t);)i.push(a),a=a.plus(r);return i}}),ji="reshape",Si=["typed","isInteger","matrix"],Ai=Object(s.a)(ji,Si,function(e){var t=e.typed,r=e.isInteger,n=e.matrix;return t(ji,{"Matrix, Array":function(e,t){return e.reshape?e.reshape(t):n(Object(I.n)(e.valueOf(),t))},"Array, Array":function(e,t){return t.forEach(function(e){if(!r(e))throw new TypeError("Invalid size for dimension: "+e)}),Object(I.n)(e,t)}})}),Ci=r(13),Ti=["config","matrix"],_i=Object(s.a)("resize",Ti,function(e){var a=e.config,o=e.matrix;return function(e,t,r){if(2!==arguments.length&&3!==arguments.length)throw new Ci.a("resize",arguments.length,2,3);if(Object(ie.v)(t)&&(t=t.valueOf()),Object(ie.e)(t[0])&&(t=t.map(function(e){return Object(ie.e)(e)?e.toNumber():e})),Object(ie.v)(e))return e.resize(t,r,!0);if("string"==typeof e)return function(e,t,r){if(void 0!==r){if("string"!=typeof r||1!==r.length)throw new TypeError("Single character expected as defaultValue")}else r=" ";if(1!==t.length)throw new D.a(t.length,1);var n=t[0];if("number"!=typeof n||!Object(j.i)(n))throw new TypeError("Invalid size, must contain positive integers (size: "+Object(J.d)(t)+")");{if(e.length>n)return e.substring(0,n);if(e.length<n){for(var i=e,a=0,o=n-e.length;a<o;a++)i+=r;return i}return e}}(e,t,r);var n=!Array.isArray(e)&&"Array"!==a.matrix;if(0===t.length){for(;Array.isArray(e);)e=e[0];return Object(ae.a)(e)}Array.isArray(e)||(e=[e]),e=Object(ae.a)(e);var i=Object(I.o)(e,t,r);return n?o(i):i}}),Ii=["typed","Index","matrix","range"],qi=Object(s.a)("row",Ii,function(e){var t=e.typed,i=e.Index,r=e.matrix,a=e.range;return t("row",{"Matrix, number":n,"Array, number":function(e,t){return n(r(Object(ae.a)(e)),t).valueOf()}});function n(e,t){if(2!==e.size().length)throw new Error("Only two dimensional matrix is supported");Object(I.s)(t,e.size()[0]);var r=a(0,e.size()[1]),n=new i(t,r);return e.subset(n)}}),Bi=["typed","config","?matrix"],ki=Object(s.a)("size",Bi,function(e){var t=e.typed,r=e.config,n=e.matrix;return t("size",{Matrix:function(e){return e.create(e.size())},Array:I.a,string:function(e){return"Array"===r.matrix?[e.length]:n([e.length])},"number | Complex | BigNumber | Unit | boolean | null":function(e){return"Array"===r.matrix?[]:n?n([]):Oi()}})}),zi="squeeze",Di=["typed","matrix"],Ri=Object(s.a)(zi,Di,function(e){var t=e.typed,r=e.matrix;return t(zi,{Array:function(e){return Object(I.p)(Object(ae.a)(e))},Matrix:function(e){var t=Object(I.p)(e.toArray());return Array.isArray(t)?r(t):t},any:function(e){return Object(ae.a)(e)}})});function Pi(e){return(Pi="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Fi(e,t){if($i(e)&&Li(e,t))return e[t];if("function"==typeof e[t]&&Hi(e,t))throw new Error('Cannot access method "'+t+'" as a property');throw new Error('No access to property "'+t+'"')}function Ui(e,t,r){if($i(e)&&Li(e,t))return e[t]=r;throw new Error('No access to property "'+t+'"')}function Li(e,t){return!(!e||"object"!==Pi(e))&&(!!Object(ae.f)(Gi,t)||!(t in Object.prototype)&&!(t in Function.prototype))}function Hi(e,t){return null!=e&&"function"==typeof e[t]&&(!(Object(ae.f)(e,t)&&Object.getPrototypeOf&&t in Object.getPrototypeOf(e))&&(!!Object(ae.f)(Zi,t)||!(t in Object.prototype)&&!(t in Function.prototype)))}function $i(e){return"object"===Pi(e)&&e&&e.constructor===Object}var Gi={length:!0,name:!0},Zi={toString:!0,valueOf:!0,toLocaleString:!0},Vi=["typed","matrix"],Ji=Object(s.a)("subset",Vi,function(e){var t=e.typed,i=e.matrix;return t("subset",{"Array, Index":function(e,t){var r=i(e).subset(t);return t.isScalar()?r:r.valueOf()},"Matrix, Index":function(e,t){return e.subset(t)},"Object, Index":Xi,"string, Index":Wi,"Array, Index, any":function(e,t,r){return i(Object(ae.a)(e)).subset(t,r,void 0).valueOf()},"Array, Index, any, any":function(e,t,r,n){return i(Object(ae.a)(e)).subset(t,r,n).valueOf()},"Matrix, Index, any":function(e,t,r){return e.clone().subset(t,r)},"Matrix, Index, any, any":function(e,t,r,n){return e.clone().subset(t,r,n)},"string, Index, string":Yi,"string, Index, string, string":Yi,"Object, Index, any":Qi})});function Wi(t,e){if(!Object(ie.t)(e))throw new TypeError("Index expected");if(1!==e.size().length)throw new D.a(e.size().length,1);var r=t.length;Object(I.s)(e.min()[0],r),Object(I.s)(e.max()[0],r);var n=e.dimension(0),i="";return n.forEach(function(e){i+=t.charAt(e)}),i}function Yi(e,t,r,n){if(!t||!0!==t.isIndex)throw new TypeError("Index expected");if(1!==t.size().length)throw new D.a(t.size().length,1);if(void 0!==n){if("string"!=typeof n||1!==n.length)throw new TypeError("Single character expected as defaultValue")}else n=" ";var i=t.dimension(0);if(i.size()[0]!==r.length)throw new D.a(i.size()[0],r.length);var a=e.length;Object(I.s)(t.min()[0]),Object(I.s)(t.max()[0]);for(var o=[],s=0;s<a;s++)o[s]=e.charAt(s);if(i.forEach(function(e,t){o[e]=r.charAt(t[0])}),o.length>a)for(var u=a-1,c=o.length;u<c;u++)o[u]||(o[u]=n);return o.join("")}function Xi(e,t){if(1!==t.size().length)throw new D.a(t.size(),1);var r=t.dimension(0);if("string"!=typeof r)throw new TypeError("String expected as index to retrieve an object property");return Fi(e,r)}function Qi(e,t,r){if(1!==t.size().length)throw new D.a(t.size(),1);var n=t.dimension(0);if("string"!=typeof n)throw new TypeError("String expected as index to retrieve an object property");var i=Object(ae.a)(e);return Ui(i,n,r),i}var Ki=["typed","matrix"],ea=Object(s.a)("transpose",Ki,function(e){var t=e.typed,r=e.matrix,n=t("transpose",{Array:function(e){return n(r(e)).valueOf()},Matrix:function(e){var t,r=e.size();switch(r.length){case 1:t=e.clone();break;case 2:var n=r[0],i=r[1];if(0===i)throw new RangeError("Cannot transpose a 2D matrix with no columns (size: "+Object(J.d)(r)+")");switch(e.storage()){case"dense":t=function(e,t,r){for(var n,i=e._data,a=[],o=0;o<r;o++){n=a[o]=[];for(var s=0;s<t;s++)n[s]=Object(ae.a)(i[s][o])}return e.createDenseMatrix({data:a,size:[r,t],datatype:e._datatype})}(e,n,i);break;case"sparse":t=function(e,t,r){for(var n,i,a,o=e._values,s=e._index,u=e._ptr,c=o?[]:void 0,f=[],l=[],p=[],m=0;m<t;m++)p[m]=0;for(n=0,i=s.length;n<i;n++)p[s[n]]++;for(var h=0,d=0;d<t;d++)l.push(h),h+=p[d],p[d]=l[d];for(l.push(h),a=0;a<r;a++)for(var y=u[a],g=u[a+1],v=y;v<g;v++){var b=p[s[v]]++;f[b]=a,o&&(c[b]=Object(ae.a)(o[v]))}return e.createSparseMatrix({values:c,index:f,ptr:l,size:[r,t],datatype:e._datatype})}(e,n,i)}break;default:throw new RangeError("Matrix must be a vector or two dimensional (size: "+Object(J.d)(this._size)+")")}return t},any:function(e){return Object(ae.a)(e)}});return n}),ta="ctranspose",ra=["typed","transpose","conj"],na=Object(s.a)(ta,ra,function(e){var t=e.typed,r=e.transpose,n=e.conj;return t(ta,{any:function(e){return n(r(e))}})}),ia=["typed","config","matrix","BigNumber"],aa=Object(s.a)("zeros",ia,function(e){var t=e.typed,r=e.config,a=e.matrix,o=e.BigNumber;return t("zeros",{"":function(){return"Array"===r.matrix?n([]):n([],"default")},"...number | BigNumber | string":function(e){if("string"!=typeof e[e.length-1])return"Array"===r.matrix?n(e):n(e,"default");var t=e.pop();return n(e,t)},Array:n,Matrix:function(e){var t=e.storage();return n(e.valueOf(),t)},"Array | Matrix, string":function(e,t){return n(e.valueOf(),t)}});function n(e,t){var r=function(e){var n=!1;return e.forEach(function(e,t,r){Object(ie.e)(e)&&(n=!0,r[t]=e.toNumber())}),n}(e)?new o(0):0;if(function(e){e.forEach(function(e){if("number"!=typeof e||!Object(j.i)(e)||e<0)throw new Error("Parameters in function zeros must be positive integers")})}(e),t){var n=a(t);return 0<e.length?n.resize(e,r):n}var i=[];return 0<e.length?Object(I.o)(i,e,r):i}}),oa=["typed"],sa=Object(s.a)("erf",oa,function(e){var t=(0,e.typed)("name",{number:function(e){var t=Math.abs(e);return pa<=t?Object(j.n)(e):t<=ua?Object(j.n)(e)*function(e){var t,r=e*e,n=fa[0][4]*r,i=r;for(t=0;t<3;t+=1)n=(n+fa[0][t])*r,i=(i+la[0][t])*r;return e*(n+fa[0][3])/(i+la[0][3])}(t):t<=4?Object(j.n)(e)*(1-function(e){var t,r=fa[1][8]*e,n=e;for(t=0;t<7;t+=1)r=(r+fa[1][t])*e,n=(n+la[1][t])*e;var i=(r+fa[1][7])/(n+la[1][7]),a=parseInt(16*e)/16,o=(e-a)*(e+a);return Math.exp(-a*a)*Math.exp(-o)*i}(t)):Object(j.n)(e)*(1-function(e){var t,r=1/(e*e),n=fa[2][5]*r,i=r;for(t=0;t<4;t+=1)n=(n+fa[2][t])*r,i=(i+la[2][t])*r;var a=r*(n+fa[2][4])/(i+la[2][4]);a=(ca-a)/e,r=parseInt(16*e)/16;var o=(e-r)*(e+r);return Math.exp(-r*r)*Math.exp(-o)*a}(t))},"Array | Matrix":function(e){return oe(e,t)}});return t}),ua=.46875,ca=.5641895835477563,fa=[[3.1611237438705655,113.86415415105016,377.485237685302,3209.3775891384694,.18577770618460315],[.5641884969886701,8.883149794388377,66.11919063714163,298.6351381974001,881.952221241769,1712.0476126340707,2051.0783778260716,1230.3393547979972,2.1531153547440383e-8],[.30532663496123236,.36034489994980445,.12578172611122926,.016083785148742275,.0006587491615298378,.016315387137302097]],la=[[23.601290952344122,244.02463793444417,1282.6165260773723,2844.236833439171],[15.744926110709835,117.6939508913125,537.1811018620099,1621.3895745666903,3290.7992357334597,4362.619090143247,3439.3676741437216,1230.3393548037495],[2.568520192289822,1.8729528499234604,.5279051029514285,.06051834131244132,.0023352049762686918]],pa=Math.pow(2,53),ma=["typed","isNaN","isNumeric"],ha=Object(s.a)("mode",ma,function(e){var t=e.typed,o=e.isNaN,s=e.isNumeric;return t("mode",{"Array | Matrix":r,"...":function(e){return r(e)}});function r(e){if(0===(e=Object(I.e)(e.valueOf())).length)throw new Error("Cannot calculate mode of an empty array");for(var t={},r=[],n=0,i=0;i<e.length;i++){var a=e[i];if(s(a)&&o(a))throw new Error("Cannot calculate mode of an array containing NaN values");a in t||(t[a]=0),t[a]++,t[a]===n?r.push(a):t[a]>n&&(n=t[a],r=[a])}return r}});function da(e,t,r){var n;return-1!==String(e).indexOf("Unexpected type")?(n=2<arguments.length?" (type: "+Object(ie.M)(r)+", value: "+JSON.stringify(r)+")":" (type: "+e.data.actual+")",new TypeError("Cannot calculate "+t+", unexpected type of argument"+n)):-1!==String(e).indexOf("complex numbers")?(n=2<arguments.length?" (type: "+Object(ie.M)(r)+", value: "+JSON.stringify(r)+")":"",new TypeError("Cannot calculate "+t+", no ordering relation is defined for complex numbers"+n)):e}var ya=["typed","multiply"],ga=Object(s.a)("prod",ya,function(e){var t=e.typed,n=e.multiply;return t("prod",{"Array | Matrix":r,"Array | Matrix, number | BigNumber":function(e,t){throw new Error("prod(A, dim) is not yet supported")},"...":function(e){return r(e)}});function r(e){var r;if(F(e,function(t){try{r=void 0===r?t:n(r,t)}catch(e){throw da(e,"prod",t)}}),void 0===r)throw new Error("Cannot calculate prod of an empty array");return r}}),va=["typed"],ba=Object(s.a)("format",va,function(e){return(0,e.typed)("format",{any:J.d,"any, Object | function | number":J.d})}),xa=["typed"],wa=Object(s.a)("print",xa,function(e){return(0,e.typed)("print",{"string, Object | Array":Na,"string, Object | Array, number | Object":Na})});function Na(e,a,o){return e.replace(/\$([\w.]+)/g,function(e,t){for(var r=t.split("."),n=a[r.shift()];r.length&&void 0!==n;){var i=r.shift();n=i?n[i]:n+"."}return void 0!==n?Object(ie.I)(n)?n:Object(J.d)(n,o):e})}var Oa=["typed","matrix"],Ma=Object(s.a)("to",Oa,function(e){var t=e.typed,r=e.matrix,n=Xt({typed:t}),i=Kt({typed:t}),a=t("to",{"Unit, Unit | string":function(e,t){return e.to(t)},"Matrix, Matrix":function(e,t){return n(e,t,a)},"Array, Array":function(e,t){return a(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return a(r(e),t)},"Matrix, Array":function(e,t){return a(e,r(t))},"Matrix, any":function(e,t){return i(e,t,a,!1)},"any, Matrix":function(e,t){return i(t,e,a,!0)},"Array, any":function(e,t){return i(r(e),t,a,!1).valueOf()},"any, Array":function(e,t){return i(r(t),e,a,!0).valueOf()}});return a}),Ea="isPrime",ja=["typed"],Sa=Object(s.a)(Ea,ja,function(e){var t=(0,e.typed)(Ea,{number:function(e){if(0*e!=0)return!1;if(e<=3)return 1<e;if(e%2==0||e%3==0)return!1;for(var t=5;t*t<=e;t+=6)if(e%t==0||e%(t+2)==0)return!1;return!0},BigNumber:function(e){if(0*e.toNumber()!=0)return!1;if(e.lte(3))return e.gt(1);if(e.mod(2).eq(0)||e.mod(3).eq(0))return!1;for(var t=5;e.gte(t*t);t+=6)if(e.mod(t).eq(0)||e.mod(t+2).eq(0))return!1;return!0},"Array | Matrix":function(e){return oe(e,t)}});return t}),Aa=["number","?bignumber","?fraction"],Ca=Object(s.a)("numeric",Aa,function(e){var t=e.number,r=e.bignumber,n=e.fraction,i={string:!0,number:!0,BigNumber:!0,Fraction:!0},a={number:function(e){return t(e)},BigNumber:r?function(e){return r(e)}:wi,Fraction:n?function(e){return n(e)}:Ni};return function(e,t){var r=Object(ie.M)(e);if(!(r in i))throw new TypeError("Cannot convert "+e+' of type "'+r+'"; valid input types are '+Object.keys(i).join(", "));if(!(t in a))throw new TypeError("Cannot convert "+e+' to type "'+t+'"; valid output types are '+Object.keys(a).join(", "));return t===r?e:a[t](e)}}),Ta="divideScalar",_a=["typed","numeric"],Ia=Object(s.a)(Ta,_a,function(e){var t=e.typed,i=e.numeric,a=t(Ta,{"number, number":function(e,t){return e/t},"Complex, Complex":function(e,t){return e.div(t)},"BigNumber, BigNumber":function(e,t){return e.div(t)},"Fraction, Fraction":function(e,t){return e.div(t)},"Unit, number | Fraction | BigNumber":function(e,t){var r=e.clone(),n=i(1,Object(ie.M)(t));return r.value=a(null===r.value?r._normalize(n):r.value,t),r},"number | Fraction | BigNumber, Unit":function(e,t){var r=t.clone();r=r.pow(-1);var n=i(1,Object(ie.M)(e));return r.value=a(e,null===t.value?t._normalize(n):t.value),r},"Unit, Unit":function(e,t){return e.divide(t)}});return a}),qa=["typed","config","identity","multiply","matrix","fraction","number","Complex"],Ba=Object(s.a)("pow",qa,function(e){var t=e.typed,i=e.config,a=e.identity,o=e.multiply,r=e.matrix,s=e.number,u=e.fraction,c=e.Complex;return t("pow",{"number, number":n,"Complex, Complex":function(e,t){return e.pow(t)},"BigNumber, BigNumber":function(e,t){return t.isInteger()||0<=e||i.predictable?e.pow(t):new c(e.toNumber(),0).pow(t.toNumber(),0)},"Fraction, Fraction":function(e,t){if(1===t.d)return e.pow(t);if(i.predictable)throw new Error("Function pow does not support non-integer exponents for fractions.");return n(e.valueOf(),t.valueOf())},"Array, number":f,"Array, BigNumber":function(e,t){return f(e,t.toNumber())},"Matrix, number":l,"Matrix, BigNumber":function(e,t){return l(e,t.toNumber())},"Unit, number | BigNumber":function(e,t){return e.pow(t)}});function n(e,t){if(i.predictable&&!Object(j.i)(t)&&e<0)try{var r=u(t),n=s(r);if((t===n||Math.abs((t-n)/t)<1e-14)&&r.d%2==1)return(r.n%2==0?1:-1)*Math.pow(-e,t)}catch(e){}return i.predictable&&(e<-1&&t===1/0||-1<e&&e<0&&t===-1/0)?NaN:Object(j.i)(t)||0<=e||i.predictable?ht(e,t):e*e<1&&t===1/0||1<e*e&&t===-1/0?0:new c(e,0).pow(t,0)}function f(e,t){if(!Object(j.i)(t)||t<0)throw new TypeError("For A^b, b must be a positive integer (value is "+t+")");var r=Object(I.a)(e);if(2!==r.length)throw new Error("For A^b, A must be 2 dimensional (A has "+r.length+" dimensions)");if(r[0]!==r[1])throw new Error("For A^b, A must be square (size is "+r[0]+"x"+r[1]+")");for(var n=a(r[0]).valueOf(),i=e;1<=t;)1==(1&t)&&(n=o(i,n)),t>>=1,i=o(i,i);return n}function l(e,t){return r(f(e.valueOf(),t))}});function ka(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)}return r}function za(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var Da="Number of decimals in function round must be an integer",Ra="round",Pa=["typed","matrix","equalScalar","zeros","BigNumber","DenseMatrix"],Fa=Object(s.a)(Ra,Pa,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.BigNumber,o=e.DenseMatrix,s=sr({typed:t,equalScalar:n}),u=br({typed:t,DenseMatrix:o}),c=Kt({typed:t}),f=t(Ra,function(t){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?ka(r,!0).forEach(function(e){za(t,e,r[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):ka(r).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))})}return t}({},Ua,{Complex:function(e){return e.round()},"Complex, number":function(e,t){if(t%1)throw new TypeError(Da);return e.round(t)},"Complex, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Da);var r=t.toNumber();return e.round(r)},"number, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Da);return new a(e).toDecimalPlaces(t.toNumber())},BigNumber:function(e){return e.toDecimalPlaces(0)},"BigNumber, BigNumber":function(e,t){if(!t.isInteger())throw new TypeError(Da);return e.toDecimalPlaces(t.toNumber())},Fraction:function(e){return e.round()},"Fraction, number":function(e,t){if(t%1)throw new TypeError(Da);return e.round(t)},"Array | Matrix":function(e){return oe(e,f,!0)},"SparseMatrix, number | BigNumber":function(e,t){return s(e,t,f,!1)},"DenseMatrix, number | BigNumber":function(e,t){return c(e,t,f,!1)},"number | Complex | BigNumber, SparseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):u(t,e,f,!0)},"number | Complex | BigNumber, DenseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):c(t,e,f,!0)},"Array, number | BigNumber":function(e,t){return c(r(e),t,f,!1).valueOf()},"number | Complex | BigNumber, Array":function(e,t){return c(r(t),e,f,!0).valueOf()}}));return f}),Ua={number:dt,"number, number":function(e,t){if(!Object(j.i)(t))throw new TypeError(Da);if(t<0||15<t)throw new Error("Number of decimals in function round must be in te range of 0-15");return dt(e,t)}},La=["config","typed","divideScalar","Complex"],Ha=Object(s.a)("log",La,function(e){var t=e.typed,r=e.config,n=e.divideScalar,i=e.Complex,a=t("log",{number:function(e){return 0<=e||r.predictable?ot(e):new i(e,0).log()},Complex:function(e){return e.log()},BigNumber:function(e){return!e.isNegative()||r.predictable?e.ln():new i(e.toNumber(),0).log()},"Array | Matrix":function(e){return oe(e,a)},"any, any":function(e,t){return n(a(e),a(t))}});return a}),$a=["typed","config","divideScalar","log","Complex"],Ga=Object(s.a)("log1p",$a,function(e){var t=e.typed,r=e.config,n=e.divideScalar,i=e.log,a=e.Complex,o=t("log1p",{number:function(e){return-1<=e||r.predictable?Object(j.k)(e):s(new a(e,0))},Complex:s,BigNumber:function(e){var t=e.plus(1);return!t.isNegative()||r.predictable?t.ln():s(new a(e.toNumber(),0))},"Array | Matrix":function(e){return oe(e,o)},"any, any":function(e,t){return n(o(e),i(t))}});function s(e){var t=e.re+1;return new a(Math.log(Math.sqrt(t*t+e.im*e.im)),Math.atan2(e.im,t))}return o}),Za="nthRoots",Va=["config","typed","divideScalar","Complex"],Ja=Object(s.a)(Za,Va,function(e){var t=e.typed,f=(e.config,e.divideScalar,e.Complex),r=t(Za,{Complex:function(e){return n(e,2)},"Complex, number":n}),l=[function(e){return new f(e,0)},function(e){return new f(0,e)},function(e){return new f(-e,0)},function(e){return new f(0,-e)}];function n(e,t){if(t<0)throw new Error("Root must be greater than zero");if(0===t)throw new Error("Root must be non-zero");if(t%1!=0)throw new Error("Root must be an integer");if(0===e||0===e.abs())return[new f(0,0)];var r,n="number"==typeof e;!n&&0!==e.re&&0!==e.im||(r=n?2*+(e<0):0===e.im?2*+(e.re<0):2*+(e.im<0)+1);for(var i=e.arg(),a=e.abs(),o=[],s=Math.pow(a,1/t),u=0;u<t;u++){var c=(r+4*u)/t;c!==Math.round(c)?o.push(new f({r:s,phi:(i+2*Math.PI*u)/t})):o.push(l[c%4](s))}return o}return r}),Wa=["typed","equalScalar","matrix","pow","DenseMatrix"],Ya=Object(s.a)("dotPow",Wa,function(e){var t=e.typed,r=e.equalScalar,n=e.matrix,i=e.pow,a=e.DenseMatrix,o=dr({typed:t}),s=gn({typed:t,DenseMatrix:a}),u=sr({typed:t,equalScalar:r}),c=br({typed:t,DenseMatrix:a}),f=Xt({typed:t}),l=Kt({typed:t}),p=t("dotPow",{"any, any":i,"SparseMatrix, SparseMatrix":function(e,t){return s(e,t,i,!1)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,i,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,i,!1)},"DenseMatrix, DenseMatrix":function(e,t){return f(e,t,i)},"Array, Array":function(e,t){return p(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return p(n(e),t)},"Matrix, Array":function(e,t){return p(e,n(t))},"SparseMatrix, any":function(e,t){return u(e,t,p,!1)},"DenseMatrix, any":function(e,t){return l(e,t,p,!1)},"any, SparseMatrix":function(e,t){return c(t,e,p,!0)},"any, DenseMatrix":function(e,t){return l(t,e,p,!0)},"Array, any":function(e,t){return l(n(e),t,p,!1).valueOf()},"any, Array":function(e,t){return l(n(t),e,p,!0).valueOf()}});return p}),Xa="dotDivide",Qa=["typed","matrix","equalScalar","divideScalar","DenseMatrix"],Ka=Object(s.a)(Xa,Qa,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.divideScalar,a=e.DenseMatrix,o=nr({typed:t,equalScalar:n}),s=dr({typed:t}),u=gn({typed:t,DenseMatrix:a}),c=sr({typed:t,equalScalar:n}),f=br({typed:t,DenseMatrix:a}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(Xa,{"any, any":i,"SparseMatrix, SparseMatrix":function(e,t){return u(e,t,i,!1)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,i,!0)},"DenseMatrix, SparseMatrix":function(e,t){return s(e,t,i,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,i)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, any":function(e,t){return c(e,t,i,!1)},"DenseMatrix, any":function(e,t){return p(e,t,i,!1)},"any, SparseMatrix":function(e,t){return f(t,e,i,!0)},"any, DenseMatrix":function(e,t){return p(t,e,i,!0)},"Array, any":function(e,t){return p(r(e),t,i,!1).valueOf()},"any, Array":function(e,t){return p(r(t),e,i,!0).valueOf()}});return m});function eo(e){var d=e.DenseMatrix;return function(e,t,r){var n=e.size();if(2!==n.length)throw new RangeError("Matrix must be two dimensional (size: "+Object(J.d)(n)+")");var i,a,o,s=n[0];if(s!==n[1])throw new RangeError("Matrix must be square (size: "+Object(J.d)(n)+")");if(Object(ie.v)(t)){var u=t.size();if(1===u.length){if(u[0]!==s)throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");for(i=[],o=t._data,a=0;a<s;a++)i[a]=[o[a]];return new d({data:i,size:[s,1],datatype:t._datatype})}if(2!==u.length)throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");if(u[0]!==s||1!==u[1])throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");if(Object(ie.n)(t)){if(r){for(i=[],o=t._data,a=0;a<s;a++)i[a]=[o[a][0]];return new d({data:i,size:[s,1],datatype:t._datatype})}return t}for(i=[],a=0;a<s;a++)i[a]=[0];for(var c=t._values,f=t._index,l=t._ptr,p=l[1],m=l[0];m<p;m++)i[a=f[m]][0]=c[m];return new d({data:i,size:[s,1],datatype:t._datatype})}if(Object(ie.b)(t)){var h=Object(I.a)(t);if(1===h.length){if(h[0]!==s)throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");for(i=[],a=0;a<s;a++)i[a]=[t[a]];return new d({data:i,size:[s,1]})}if(2!==h.length)throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");if(h[0]!==s||1!==h[1])throw new RangeError("Dimension mismatch. Matrix columns must match vector length.");for(i=[],a=0;a<s;a++)i[a]=[t[a][0]];return new d({data:i,size:[s,1]})}}}var to=["typed","matrix","divideScalar","multiplyScalar","subtract","equalScalar","DenseMatrix"],ro=Object(s.a)("lsolve",to,function(e){var t=e.typed,r=e.matrix,v=e.divideScalar,b=e.multiplyScalar,x=e.subtract,w=e.equalScalar,N=e.DenseMatrix,O=eo({DenseMatrix:N});return t("lsolve",{"SparseMatrix, Array | Matrix":function(e,t){return function(e,t){for(var r,n,i=(t=O(e,t,!0))._data,a=e._size[0],o=e._size[1],s=e._values,u=e._index,c=e._ptr,f=[],l=0;l<o;l++){var p=i[l][0]||0;if(w(p,0))f[l]=[0];else{var m=0,h=[],d=[],y=c[l+1];for(n=c[l];n<y;n++)(r=u[n])===l?m=s[n]:l<r&&(h.push(s[n]),d.push(r));if(w(m,0))throw new Error("Linear system cannot be solved since matrix is singular");var g=v(p,m);for(n=0,y=d.length;n<y;n++)r=d[n],i[r]=[x(i[r][0]||0,b(g,h[n]))];f[l]=[g]}}return new N({data:f,size:[a,1]})}(e,t)},"DenseMatrix, Array | Matrix":function(e,t){return n(e,t)},"Array, Array | Matrix":function(e,t){return n(r(e),t).valueOf()}});function n(e,t){for(var r=(t=O(e,t,!0))._data,n=e._size[0],i=e._size[1],a=[],o=e._data,s=0;s<i;s++){var u=r[s][0]||0,c=void 0;if(w(u,0))c=0;else{var f=o[s][s];if(w(f,0))throw new Error("Linear system cannot be solved since matrix is singular");c=v(u,f);for(var l=s+1;l<n;l++)r[l]=[x(r[l][0]||0,b(c,o[l][s]))]}a[s]=[c]}return new N({data:a,size:[n,1]})}}),no=["typed","matrix","divideScalar","multiplyScalar","subtract","equalScalar","DenseMatrix"],io=Object(s.a)("usolve",no,function(e){var t=e.typed,r=e.matrix,b=e.divideScalar,x=e.multiplyScalar,w=e.subtract,N=e.equalScalar,O=e.DenseMatrix,M=eo({DenseMatrix:O});return t("usolve",{"SparseMatrix, Array | Matrix":function(e,t){return function(e,t){for(var r,n,i=(t=M(e,t,!0))._data,a=e._size[0],o=e._size[1],s=e._values,u=e._index,c=e._ptr,f=[],l=o-1;0<=l;l--){var p=i[l][0]||0;if(N(p,0))f[l]=[0];else{var m=0,h=[],d=[],y=c[l],g=c[l+1];for(n=g-1;y<=n;n--)(r=u[n])===l?m=s[n]:r<l&&(h.push(s[n]),d.push(r));if(N(m,0))throw new Error("Linear system cannot be solved since matrix is singular");var v=b(p,m);for(n=0,g=d.length;n<g;n++)r=d[n],i[r]=[w(i[r][0],x(v,h[n]))];f[l]=[v]}}return new O({data:f,size:[a,1]})}(e,t)},"DenseMatrix, Array | Matrix":function(e,t){return n(e,t)},"Array, Array | Matrix":function(e,t){return n(r(e),t).valueOf()}});function n(e,t){for(var r=(t=M(e,t,!0))._data,n=e._size[0],i=e._size[1],a=[],o=e._data,s=i-1;0<=s;s--){var u=r[s][0]||0,c=void 0;if(N(u,0))c=0;else{var f=o[s][s];if(N(f,0))throw new Error("Linear system cannot be solved since matrix is singular");c=b(u,f);for(var l=s-1;0<=l;l--)r[l]=[w(r[l][0]||0,x(c,o[l][s]))]}a[s]=[c]}return new O({data:a,size:[n,1]})}}),ao=["typed","equalScalar"],oo=Object(s.a)("algorithm08",ao,function(e){var I=e.typed,q=e.equalScalar;return function(e,t,r){var n=e._values,i=e._index,a=e._ptr,o=e._size,s=e._datatype,u=t._values,c=t._index,f=t._ptr,l=t._size,p=t._datatype;if(o.length!==l.length)throw new D.a(o.length,l.length);if(o[0]!==l[0]||o[1]!==l[1])throw new RangeError("Dimension mismatch. Matrix A ("+o+") must match Matrix B ("+l+")");if(!n||!u)throw new Error("Cannot perform operation on Pattern Sparse Matrices");var m,h=o[0],d=o[1],y=q,g=0,v=r;"string"==typeof s&&s===p&&(m=s,y=I.find(q,[m,m]),g=I.convert(0,m),v=I.find(r,[m,m]));for(var b,x,w,N,O=[],M=[],E=[],j=e.createSparseMatrix({values:O,index:M,ptr:E,size:[h,d],datatype:m}),S=[],A=[],C=0;C<d;C++){E[C]=M.length;var T=C+1;for(x=a[C],w=a[C+1],b=x;b<w;b++)A[N=i[b]]=T,S[N]=n[b],M.push(N);for(x=f[C],w=f[C+1],b=x;b<w;b++)A[N=c[b]]===T&&(S[N]=v(S[N],u[b]));for(b=E[C];b<M.length;){var _=S[N=M[b]];y(_,g)?M.splice(b,1):(O.push(_),b++)}}return E[d]=M.length,j}}),so="leftShift",uo=["typed","matrix","equalScalar","zeros","DenseMatrix"],co=Object(s.a)(so,uo,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.DenseMatrix,o=Gt({typed:t}),s=nr({typed:t,equalScalar:n}),u=oo({typed:t,equalScalar:n}),c=Wt({typed:t,DenseMatrix:a}),f=sr({typed:t,equalScalar:n}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(so,{"number, number":sn,"BigNumber, BigNumber":Kr,"SparseMatrix, SparseMatrix":function(e,t){return u(e,t,m,!1)},"SparseMatrix, DenseMatrix":function(e,t){return s(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,m)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():f(e,t,m,!1)},"DenseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():p(e,t,m,!1)},"number | BigNumber, SparseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):c(t,e,m,!0)},"number | BigNumber, DenseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):p(t,e,m,!0)},"Array, number | BigNumber":function(e,t){return m(r(e),t).valueOf()},"number | BigNumber, Array":function(e,t){return m(e,r(t)).valueOf()}});return m}),fo="rightArithShift",lo=["typed","matrix","equalScalar","zeros","DenseMatrix"],po=Object(s.a)(fo,lo,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.DenseMatrix,o=Gt({typed:t}),s=nr({typed:t,equalScalar:n}),u=oo({typed:t,equalScalar:n}),c=Wt({typed:t,DenseMatrix:a}),f=sr({typed:t,equalScalar:n}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(fo,{"number, number":un,"BigNumber, BigNumber":en,"SparseMatrix, SparseMatrix":function(e,t){return u(e,t,m,!1)},"SparseMatrix, DenseMatrix":function(e,t){return s(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,m)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():f(e,t,m,!1)},"DenseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():p(e,t,m,!1)},"number | BigNumber, SparseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):c(t,e,m,!0)},"number | BigNumber, DenseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):p(t,e,m,!0)},"Array, number | BigNumber":function(e,t){return m(r(e),t).valueOf()},"number | BigNumber, Array":function(e,t){return m(e,r(t)).valueOf()}});return m}),mo="rightLogShift",ho=["typed","matrix","equalScalar","zeros","DenseMatrix"],yo=Object(s.a)(mo,ho,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.DenseMatrix,o=Gt({typed:t}),s=nr({typed:t,equalScalar:n}),u=oo({typed:t,equalScalar:n}),c=Wt({typed:t,DenseMatrix:a}),f=sr({typed:t,equalScalar:n}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(mo,{"number, number":cn,"SparseMatrix, SparseMatrix":function(e,t){return u(e,t,m,!1)},"SparseMatrix, DenseMatrix":function(e,t){return s(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,m)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():f(e,t,m,!1)},"DenseMatrix, number | BigNumber":function(e,t){return n(t,0)?e.clone():p(e,t,m,!1)},"number | BigNumber, SparseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):c(t,e,m,!0)},"number | BigNumber, DenseMatrix":function(e,t){return n(e,0)?i(t.size(),t.storage()):p(t,e,m,!0)},"Array, number | BigNumber":function(e,t){return m(r(e),t).valueOf()},"number | BigNumber, Array":function(e,t){return m(e,r(t)).valueOf()}});return m}),go=["typed","matrix","equalScalar","zeros","not"],vo=Object(s.a)("and",go,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.zeros,a=e.not,o=nr({typed:t,equalScalar:n}),s=ar({typed:t,equalScalar:n}),u=sr({typed:t,equalScalar:n}),c=Xt({typed:t}),f=Kt({typed:t}),l=t("and",{"number, number":In,"Complex, Complex":function(e,t){return!(0===e.re&&0===e.im||0===t.re&&0===t.im)},"BigNumber, BigNumber":function(e,t){return!(e.isZero()||t.isZero()||e.isNaN()||t.isNaN())},"Unit, Unit":function(e,t){return l(e.value||0,t.value||0)},"SparseMatrix, SparseMatrix":function(e,t){return s(e,t,l,!1)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,l,!0)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,l,!1)},"DenseMatrix, DenseMatrix":function(e,t){return c(e,t,l)},"Array, Array":function(e,t){return l(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return l(r(e),t)},"Matrix, Array":function(e,t){return l(e,r(t))},"SparseMatrix, any":function(e,t){return a(t)?i(e.size(),e.storage()):u(e,t,l,!1)},"DenseMatrix, any":function(e,t){return a(t)?i(e.size(),e.storage()):f(e,t,l,!1)},"any, SparseMatrix":function(e,t){return a(e)?i(e.size(),e.storage()):u(t,e,l,!0)},"any, DenseMatrix":function(e,t){return a(e)?i(e.size(),e.storage()):f(t,e,l,!0)},"Array, any":function(e,t){return l(r(e),t).valueOf()},"any, Array":function(e,t){return l(e,r(t)).valueOf()}});return l}),bo="compare",xo=["typed","config","matrix","equalScalar","BigNumber","Fraction","DenseMatrix"],wo=Object(s.a)(bo,xo,function(e){var t=e.typed,r=e.config,n=e.equalScalar,i=e.matrix,a=e.BigNumber,o=e.Fraction,s=e.DenseMatrix,u=dr({typed:t}),c=gr({typed:t,equalScalar:n}),f=br({typed:t,DenseMatrix:s}),l=Xt({typed:t}),p=Kt({typed:t}),m=t(bo,{"boolean, boolean":function(e,t){return e===t?0:t<e?1:-1},"number, number":function(e,t){return Object(j.m)(e,t,r.epsilon)?0:t<e?1:-1},"BigNumber, BigNumber":function(e,t){return Ne(e,t,r.epsilon)?new a(0):new a(e.cmp(t))},"Fraction, Fraction":function(e,t){return new o(e.compare(t))},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return m(e.value,t.value)},"SparseMatrix, SparseMatrix":function(e,t){return c(e,t,m)},"SparseMatrix, DenseMatrix":function(e,t){return u(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return u(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,m)},"Array, Array":function(e,t){return m(i(e),i(t)).valueOf()},"Array, Matrix":function(e,t){return m(i(e),t)},"Matrix, Array":function(e,t){return m(e,i(t))},"SparseMatrix, any":function(e,t){return f(e,t,m,!1)},"DenseMatrix, any":function(e,t){return p(e,t,m,!1)},"any, SparseMatrix":function(e,t){return f(t,e,m,!0)},"any, DenseMatrix":function(e,t){return p(t,e,m,!0)},"Array, any":function(e,t){return p(i(e),t,m,!1).valueOf()},"any, Array":function(e,t){return p(i(t),e,m,!0).valueOf()}});return m}),No=r(12),Oo=r.n(No),Mo="compareNatural",Eo=["typed","compare"],jo=Object(s.a)(Mo,Eo,function(e){var t=e.typed,a=e.compare,o=a.signatures["boolean,boolean"],s=t(Mo,{"any, any":function(e,t){var r,n=Object(ie.M)(e),i=Object(ie.M)(t);if(!("number"!==n&&"BigNumber"!==n&&"Fraction"!==n||"number"!==i&&"BigNumber"!==i&&"Fraction"!==i))return"0"!==(r=a(e,t)).toString()?0<r?1:-1:Oo()(n,i);if("Array"===n||"Matrix"===n||"Array"===i||"Matrix"===i)return 0!==(r=function e(t,r){if(Object(ie.H)(t)&&Object(ie.H)(r))return u(t.toJSON().values,r.toJSON().values);if(Object(ie.H)(t))return e(t.toArray(),r);if(Object(ie.H)(r))return e(t,r.toArray());if(Object(ie.n)(t))return e(t.toJSON().data,r);if(Object(ie.n)(r))return e(t,r.toJSON().data);if(!Array.isArray(t))return e([t],r);if(!Array.isArray(r))return e(t,[r]);return u(t,r)}(e,t))?r:Oo()(n,i);if(n!==i)return Oo()(n,i);if("Complex"===n)return function(e,t){if(e.re>t.re)return 1;if(e.re<t.re)return-1;if(e.im>t.im)return 1;if(e.im<t.im)return-1;return 0}(e,t);if("Unit"===n)return e.equalBase(t)?s(e.value,t.value):u(e.formatUnits(),t.formatUnits());if("boolean"===n)return o(e,t);if("string"===n)return Oo()(e,t);if("Object"===n)return function(e,t){var r=Object.keys(e),n=Object.keys(t);r.sort(Oo.a),n.sort(Oo.a);var i=u(r,n);if(0!==i)return i;for(var a=0;a<r.length;a++){var o=s(e[r[a]],t[n[a]]);if(0!==o)return o}return 0}(e,t);if("null"===n)return 0;if("undefined"===n)return 0;throw new TypeError('Unsupported type of value "'+n+'"')}});function u(e,t){for(var r=0,n=Math.min(e.length,t.length);r<n;r++){var i=s(e[r],t[r]);if(0!==i)return i}return e.length>t.length?1:e.length<t.length?-1:0}return s});var So="compareText",Ao=["typed","matrix"],Co=Object(s.a)(So,Ao,function(e){var t=e.typed,r=e.matrix,n=Xt({typed:t}),i=Kt({typed:t}),a=t(So,{"any, any":J.a,"DenseMatrix, DenseMatrix":function(e,t){return n(e,t,J.a)},"Array, Array":function(e,t){return a(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return a(r(e),t)},"Matrix, Array":function(e,t){return a(e,r(t))},"DenseMatrix, any":function(e,t){return i(e,t,J.a,!1)},"any, DenseMatrix":function(e,t){return i(t,e,J.a,!0)},"Array, any":function(e,t){return i(r(e),t,J.a,!1).valueOf()},"any, Array":function(e,t){return i(r(t),e,J.a,!0).valueOf()}});return a}),To="equal",_o=["typed","matrix","equalScalar","DenseMatrix"],Io=Object(s.a)(To,_o,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t(To,{"any, any":function(e,t){return null===e?null===t:null===t?null===e:void 0===e?void 0===t:void 0===t?void 0===e:n(e,t)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,n)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,n,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,n,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,n)},"Array, Array":function(e,t){return f(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return f(r(e),t)},"Matrix, Array":function(e,t){return f(e,r(t))},"SparseMatrix, any":function(e,t){return s(e,t,n,!1)},"DenseMatrix, any":function(e,t){return c(e,t,n,!1)},"any, SparseMatrix":function(e,t){return s(t,e,n,!0)},"any, DenseMatrix":function(e,t){return c(t,e,n,!0)},"Array, any":function(e,t){return c(r(e),t,n,!1).valueOf()},"any, Array":function(e,t){return c(r(t),e,n,!0).valueOf()}});return f}),qo=(Object(s.a)(To,["typed","equalScalar"],function(e){var t=e.typed,r=e.equalScalar;return t(To,{"any, any":function(e,t){return null===e?null===t:null===t?null===e:void 0===e?void 0===t:void 0===t?void 0===e:r(e,t)}})}),"equalText"),Bo=["typed","compareText","isZero"],ko=Object(s.a)(qo,Bo,function(e){var t=e.typed,r=e.compareText,n=e.isZero;return t(qo,{"any, any":function(e,t){return n(r(e,t))}})}),zo="smaller",Do=["typed","config","matrix","DenseMatrix"],Ro=Object(s.a)(zo,Do,function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t(zo,{"boolean, boolean":function(e,t){return e<t},"number, number":function(e,t){return e<t&&!Object(j.m)(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.lt(t)&&!Ne(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return-1===e.compare(t)},"Complex, Complex":function(e,t){throw new TypeError("No ordering relation is defined for complex numbers")},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return f(e.value,t.value)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return f(n(e),t)},"Matrix, Array":function(e,t){return f(e,n(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(n(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(n(t),e,f,!0).valueOf()}});return f}),Po="smallerEq",Fo=["typed","config","matrix","DenseMatrix"],Uo=Object(s.a)(Po,Fo,function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t(Po,{"boolean, boolean":function(e,t){return e<=t},"number, number":function(e,t){return e<=t||Object(j.m)(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.lte(t)||Ne(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return 1!==e.compare(t)},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return f(e.value,t.value)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return f(n(e),t)},"Matrix, Array":function(e,t){return f(e,n(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(n(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(n(t),e,f,!0).valueOf()}});return f}),Lo="larger",Ho=["typed","config","matrix","DenseMatrix"],$o=Object(s.a)(Lo,Ho,function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t(Lo,{"boolean, boolean":function(e,t){return t<e},"number, number":function(e,t){return t<e&&!Object(j.m)(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.gt(t)&&!Ne(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return 1===e.compare(t)},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return f(e.value,t.value)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return f(n(e),t)},"Matrix, Array":function(e,t){return f(e,n(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(n(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(n(t),e,f,!0).valueOf()}});return f}),Go="largerEq",Zo=["typed","config","matrix","DenseMatrix"],Vo=Object(s.a)(Go,Zo,function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t(Go,{"boolean, boolean":function(e,t){return t<=e},"number, number":function(e,t){return t<=e||Object(j.m)(e,t,r.epsilon)},"BigNumber, BigNumber":function(e,t){return e.gte(t)||Ne(e,t,r.epsilon)},"Fraction, Fraction":function(e,t){return-1!==e.compare(t)},"Complex, Complex":function(){throw new TypeError("No ordering relation is defined for complex numbers")},"Unit, Unit":function(e,t){if(!e.equalBase(t))throw new Error("Cannot compare units with different base");return f(e.value,t.value)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,f)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,f,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,f,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,f)},"Array, Array":function(e,t){return f(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return f(n(e),t)},"Matrix, Array":function(e,t){return f(e,n(t))},"SparseMatrix, any":function(e,t){return s(e,t,f,!1)},"DenseMatrix, any":function(e,t){return c(e,t,f,!1)},"any, SparseMatrix":function(e,t){return s(t,e,f,!0)},"any, DenseMatrix":function(e,t){return c(t,e,f,!0)},"Array, any":function(e,t){return c(n(e),t,f,!1).valueOf()},"any, Array":function(e,t){return c(n(t),e,f,!0).valueOf()}});return f}),Jo="deepEqual",Wo=["typed","equal"],Yo=Object(s.a)(Jo,Wo,function(e){var t=e.typed,a=e.equal;return t(Jo,{"any, any":function(e,t){return function e(t,r){{if(Array.isArray(t)){if(Array.isArray(r)){var n=t.length;if(n!==r.length)return!1;for(var i=0;i<n;i++)if(!e(t[i],r[i]))return!1;return!0}return!1}return!Array.isArray(r)&&a(t,r)}}(e.valueOf(),t.valueOf())}})}),Xo="unequal",Qo=["typed","config","equalScalar","matrix","DenseMatrix"],Ko=Object(s.a)(Xo,Qo,function(e){var t=e.typed,r=(e.config,e.equalScalar),n=e.matrix,i=e.DenseMatrix,a=dr({typed:t}),o=gn({typed:t,DenseMatrix:i}),s=br({typed:t,DenseMatrix:i}),u=Xt({typed:t}),c=Kt({typed:t}),f=t("unequal",{"any, any":function(e,t){return null===e?null!==t:null===t?null!==e:void 0===e?void 0!==t:void 0===t?void 0!==e:l(e,t)},"SparseMatrix, SparseMatrix":function(e,t){return o(e,t,l)},"SparseMatrix, DenseMatrix":function(e,t){return a(t,e,l,!0)},"DenseMatrix, SparseMatrix":function(e,t){return a(e,t,l,!1)},"DenseMatrix, DenseMatrix":function(e,t){return u(e,t,l)},"Array, Array":function(e,t){return f(n(e),n(t)).valueOf()},"Array, Matrix":function(e,t){return f(n(e),t)},"Matrix, Array":function(e,t){return f(e,n(t))},"SparseMatrix, any":function(e,t){return s(e,t,l,!1)},"DenseMatrix, any":function(e,t){return c(e,t,l,!1)},"any, SparseMatrix":function(e,t){return s(t,e,l,!0)},"any, DenseMatrix":function(e,t){return c(t,e,l,!0)},"Array, any":function(e,t){return c(n(e),t,l,!1).valueOf()},"any, Array":function(e,t){return c(n(t),e,l,!0).valueOf()}});function l(e,t){return!r(e,t)}return f}),es=(Object(s.a)(Xo,["typed","equalScalar"],function(e){var t=e.typed,r=e.equalScalar;return t(Xo,{"any, any":function(e,t){return null===e?null!==t:null===t?null!==e:void 0===e?void 0!==t:void 0===t?void 0!==e:!r(e,t)}})}),"partitionSelect"),ts=["typed","isNumeric","isNaN","compare"],rs=Object(s.a)(es,ts,function(e){function n(e,t){return-r(e,t)}var t=e.typed,f=e.isNumeric,l=e.isNaN,r=e.compare,i=r;return t(es,{"Array | Matrix, number":function(e,t){return a(e,t,i)},"Array | Matrix, number, string":function(e,t,r){if("asc"===r)return a(e,t,i);if("desc"===r)return a(e,t,n);throw new Error('Compare string must be "asc" or "desc"')},"Array | Matrix, number, function":a});function a(e,t,r){if(!Object(j.i)(t)||t<0)throw new Error("k must be a non-negative integer");if(Object(ie.v)(e)){if(1<e.size().length)throw new Error("Only one dimensional matrices supported");return o(e.valueOf(),t,r)}if(Array.isArray(e))return o(e,t,r)}function o(e,t,r){if(t>=e.length)throw new Error("k out of bounds");for(var n=0;n<e.length;n++)if(f(e[n])&&l(e[n]))return e[n];for(var i=0,a=e.length-1;i<a;){for(var o=i,s=a,u=e[Math.floor(Math.random()*(a-i+1))+i];o<s;)if(0<=r(e[o],u)){var c=e[s];e[s]=e[o],e[o]=c,--s}else++o;0<r(e[o],u)&&--o,t<=o?a=o:i=o+1}return e[t]}}),ns=["typed","matrix","compare","compareNatural"],is=Object(s.a)("sort",ns,function(e){function t(e,t){return-i(e,t)}var r=e.typed,n=e.matrix,i=e.compare,a=e.compareNatural,o=i;return r("sort",{Array:function(e){return u(e),e.sort(o)},Matrix:function(e){return c(e),n(e.toArray().sort(o),e.storage())},"Array, function":function(e,t){return u(e),e.sort(t)},"Matrix, function":function(e,t){return c(e),n(e.toArray().sort(t),e.storage())},"Array, string":function(e,t){return u(e),e.sort(s(t))},"Matrix, string":function(e,t){return c(e),n(e.toArray().sort(s(t)),e.storage())}});function s(e){if("asc"===e)return o;if("desc"===e)return t;if("natural"===e)return a;throw new Error('String "asc", "desc", or "natural" expected')}function u(e){if(1!==Object(I.a)(e).length)throw new Error("One dimensional array expected")}function c(e){if(1!==e.size().length)throw new Error("One dimensional matrix expected")}}),as=["typed","larger"],os=Object(s.a)("max",as,function(e){var t=e.typed,n=e.larger;return t("max",{"Array | Matrix":i,"Array | Matrix, number | BigNumber":function(e,t){return U(e,t.valueOf(),r)},"...":function(e){if(P(e))throw new TypeError("Scalar values expected in function max");return i(e)}});function r(e,t){try{return n(e,t)?e:t}catch(e){throw da(e,"max",t)}}function i(e){var r;if(F(e,function(t){try{isNaN(t)&&"number"==typeof t?r=NaN:void 0!==r&&!n(t,r)||(r=t)}catch(e){throw da(e,"max",t)}}),void 0===r)throw new Error("Cannot calculate max of an empty array");return r}}),ss=["typed","smaller"],us=Object(s.a)("min",ss,function(e){var t=e.typed,n=e.smaller;return t("min",{"Array | Matrix":i,"Array | Matrix, number | BigNumber":function(e,t){return U(e,t.valueOf(),r)},"...":function(e){if(P(e))throw new TypeError("Scalar values expected in function min");return i(e)}});function r(e,t){try{return n(e,t)?e:t}catch(e){throw da(e,"min",t)}}function i(e){var r;if(F(e,function(t){try{isNaN(t)&&"number"==typeof t?r=NaN:void 0!==r&&!n(t,r)||(r=t)}catch(e){throw da(e,"min",t)}}),void 0===r)throw new Error("Cannot calculate min of an empty array");return r}}),cs=["smaller","DenseMatrix"],fs=Object(s.a)("ImmutableDenseMatrix",cs,function(e){var r=e.smaller,n=e.DenseMatrix;function i(e,t){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if(t&&!Object(ie.I)(t))throw new Error("Invalid datatype: "+t);if(Object(ie.v)(e)||Object(ie.b)(e)){var r=new n(e,t);this._data=r._data,this._size=r._size,this._datatype=r._datatype,this._min=null,this._max=null}else if(e&&Object(ie.b)(e.data)&&Object(ie.b)(e.size))this._data=e.data,this._size=e.size,this._datatype=e.datatype,this._min=void 0!==e.min?e.min:null,this._max=void 0!==e.max?e.max:null;else{if(e)throw new TypeError("Unsupported type of data ("+Object(ie.M)(e)+")");this._data=[],this._size=[0],this._datatype=t,this._min=null,this._max=null}}return(i.prototype=new n).type="ImmutableDenseMatrix",i.prototype.isImmutableDenseMatrix=!0,i.prototype.subset=function(e){switch(arguments.length){case 1:var t=n.prototype.subset.call(this,e);return Object(ie.v)(t)?new i({data:t._data,size:t._size,datatype:t._datatype}):t;case 2:case 3:throw new Error("Cannot invoke set subset on an Immutable Matrix instance");default:throw new SyntaxError("Wrong number of arguments")}},i.prototype.set=function(){throw new Error("Cannot invoke set on an Immutable Matrix instance")},i.prototype.resize=function(){throw new Error("Cannot invoke resize on an Immutable Matrix instance")},i.prototype.reshape=function(){throw new Error("Cannot invoke reshape on an Immutable Matrix instance")},i.prototype.clone=function(){return new i({data:Object(ae.a)(this._data),size:Object(ae.a)(this._size),datatype:this._datatype})},i.prototype.toJSON=function(){return{mathjs:"ImmutableDenseMatrix",data:this._data,size:this._size,datatype:this._datatype}},i.fromJSON=function(e){return new i(e)},i.prototype.swapRows=function(){throw new Error("Cannot invoke swapRows on an Immutable Matrix instance")},i.prototype.min=function(){if(null===this._min){var t=null;this.forEach(function(e){null!==t&&!r(e,t)||(t=e)}),this._min=null!==t?t:void 0}return this._min},i.prototype.max=function(){if(null===this._max){var t=null;this.forEach(function(e){null!==t&&!r(t,e)||(t=e)}),this._max=null!==t?t:void 0}return this._max},i},{isClass:!0}),ls=["ImmutableDenseMatrix"],ps=Object(s.a)("Index",ls,function(e){var n=e.ImmutableDenseMatrix;function o(e){if(!(this instanceof o))throw new SyntaxError("Constructor must be called with the new operator");this._dimensions=[],this._isScalar=!0;for(var t=0,r=arguments.length;t<r;t++){var n=arguments[t];if(Object(ie.D)(n))this._dimensions.push(n),this._isScalar=!1;else if(Array.isArray(n)||Object(ie.v)(n)){var i=s(n.valueOf());this._dimensions.push(i);var a=i.size();1===a.length&&1===a[0]||(this._isScalar=!1)}else if("number"==typeof n)this._dimensions.push(s([n]));else{if("string"!=typeof n)throw new TypeError("Dimension must be an Array, Matrix, number, string, or Range");this._dimensions.push(n)}}}function s(e){for(var t=0,r=e.length;t<r;t++)if("number"!=typeof e[t]||!Object(j.i)(e[t]))throw new TypeError("Index parameters must be positive integer numbers");return new n(e)}return o.prototype.type="Index",o.prototype.isIndex=!0,o.prototype.clone=function(){var e=new o;return e._dimensions=Object(ae.a)(this._dimensions),e._isScalar=this._isScalar,e},o.create=function(e){var t=new o;return o.apply(t,e),t},o.prototype.size=function(){for(var e=[],t=0,r=this._dimensions.length;t<r;t++){var n=this._dimensions[t];e[t]="string"==typeof n?1:n.size()[0]}return e},o.prototype.max=function(){for(var e=[],t=0,r=this._dimensions.length;t<r;t++){var n=this._dimensions[t];e[t]="string"==typeof n?n:n.max()}return e},o.prototype.min=function(){for(var e=[],t=0,r=this._dimensions.length;t<r;t++){var n=this._dimensions[t];e[t]="string"==typeof n?n:n.min()}return e},o.prototype.forEach=function(e){for(var t=0,r=this._dimensions.length;t<r;t++)e(this._dimensions[t],t,this)},o.prototype.dimension=function(e){return this._dimensions[e]||null},o.prototype.isObjectProperty=function(){return 1===this._dimensions.length&&"string"==typeof this._dimensions[0]},o.prototype.getObjectProperty=function(){return this.isObjectProperty()?this._dimensions[0]:null},o.prototype.isScalar=function(){return this._isScalar},o.prototype.valueOf=o.prototype.toArray=function(){for(var e=[],t=0,r=this._dimensions.length;t<r;t++){var n=this._dimensions[t];e.push("string"==typeof n?n:n.toArray())}return e},o.prototype.toString=function(){for(var e=[],t=0,r=this._dimensions.length;t<r;t++){var n=this._dimensions[t];"string"==typeof n?e.push(JSON.stringify(n)):e.push(n.toString())}return"["+e.join(", ")+"]"},o.prototype.toJSON=function(){return{mathjs:"Index",dimensions:this._dimensions}},o.fromJSON=function(e){return o.create(e.dimensions)},o},{isClass:!0}),ms=["smaller","larger"],hs=Object(s.a)("FibonacciHeap",ms,function(e){var l=e.smaller,p=e.larger,m=1/Math.log((1+Math.sqrt(5))/2);function t(){if(!(this instanceof t))throw new SyntaxError("Constructor must be called with the new operator");this._minimum=null,this._size=0}function i(e,t,r){t.left.right=t.right,t.right.left=t.left,r.degree--,r.child===t&&(r.child=t.right),0===r.degree&&(r.child=null),t.left=e,t.right=e.right,((e.right=t).right.left=t).parent=null,t.mark=!1}t.prototype.type="FibonacciHeap",t.prototype.isFibonacciHeap=!0,t.prototype.insert=function(e,t){var r={key:e,value:t,degree:0};if(this._minimum){var n=this._minimum;r.left=n,r.right=n.right,(n.right=r).right.left=r,l(e,n.key)&&(this._minimum=r)}else(r.left=r).right=r,this._minimum=r;return this._size++,r},t.prototype.size=function(){return this._size},t.prototype.clear=function(){this._minimum=null,this._size=0},t.prototype.isEmpty=function(){return 0===this._size},t.prototype.extractMinimum=function(){var e=this._minimum;if(null===e)return e;for(var t=this._minimum,r=e.degree,n=e.child;0<r;){var i=n.right;n.left.right=n.right,n.right.left=n.left,n.left=t,n.right=t.right,((t.right=n).right.left=n).parent=null,n=i,r--}return e.left.right=e.right,e.right.left=e.left,t=e===e.right?null:function(e,t){var r,n=Math.floor(Math.log(t)*m)+1,i=new Array(n),a=0,o=e;if(o)for(a++,o=o.right;o!==e;)a++,o=o.right;for(;0<a;){for(var s=o.degree,u=o.right;r=i[s];){if(p(o.key,r.key)){var c=r;r=o,o=c}h(r,o),i[s]=null,s++}i[s]=o,o=u,a--}e=null;for(var f=0;f<n;f++)(r=i[f])&&(e?(r.left.right=r.right,r.right.left=r.left,r.left=e,r.right=e.right,(e.right=r).right.left=r,l(r.key,e.key)&&(e=r)):e=r);return e}(t=e.right,this._size),this._size--,this._minimum=t,e},t.prototype.remove=function(e){this._minimum=function(e,t,r){t.key=r;var n=t.parent;n&&l(t.key,n.key)&&(i(e,t,n),function e(t,r){var n=r.parent;if(!n)return;r.mark?(i(t,r,n),e(n)):r.mark=!0}(e,n));l(t.key,e.key)&&(e=t);return e}(this._minimum,e,-1),this.extractMinimum()};var h=function(e,t){e.left.right=e.right,e.right.left=e.left,(e.parent=t).child?(e.left=t.child,e.right=t.child.right,(t.child.right=e).right.left=e):((t.child=e).right=e).left=e,t.degree++,e.mark=!1};return t},{isClass:!0}),ds=["addScalar","equalScalar","FibonacciHeap"],ys=Object(s.a)("Spa",ds,function(e){var n=e.addScalar,c=e.equalScalar,t=e.FibonacciHeap;function r(){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");this._values=[],this._heap=new t}return r.prototype.type="Spa",r.prototype.isSpa=!0,r.prototype.set=function(e,t){if(this._values[e])this._values[e].value=t;else{var r=this._heap.insert(e,t);this._values[e]=r}},r.prototype.get=function(e){var t=this._values[e];return t?t.value:0},r.prototype.accumulate=function(e,t){var r=this._values[e];r?r.value=n(r.value,t):(r=this._heap.insert(e,t),this._values[e]=r)},r.prototype.forEach=function(e,t,r){var n=this._heap,i=this._values,a=[],o=n.extractMinimum();for(o&&a.push(o);o&&o.key<=t;)o.key>=e&&(c(o.value,0)||r(o.key,o.value,this)),(o=n.extractMinimum())&&a.push(o);for(var s=0;s<a.length;s++){var u=a[s];i[(o=n.insert(u.key,u.value)).key]=o}},r.prototype.swap=function(e,t){var r=this._values[e],n=this._values[t];if(!r&&n)r=this._heap.insert(e,n.value),this._heap.remove(n),this._values[e]=r,this._values[t]=void 0;else if(r&&!n)n=this._heap.insert(t,r.value),this._heap.remove(r),this._values[t]=n,this._values[e]=void 0;else if(r&&n){var i=r.value;r.value=n.value,n.value=i}},r},{isClass:!0}),gs=Yn(function(e){return new e(1).exp()},ws),vs=Yn(function(e){return new e(1).plus(new e(5).sqrt()).div(2)},ws),bs=Yn(function(e){return e.acos(-1)},ws),xs=Yn(function(e){return bs(e).times(2)},ws);function ws(e){return e[0].precision}function Ns(e){return(Ns="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Os(){return(Os=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}var Ms=["?on","config","addScalar","subtract","multiplyScalar","divideScalar","pow","abs","fix","round","equal","isNumeric","format","number","Complex","BigNumber","Fraction"],Es=Object(s.a)("Unit",Ms,function(e){var y,g,v,t=e.on,b=e.config,c=e.addScalar,f=e.subtract,l=e.multiplyScalar,p=e.divideScalar,m=e.pow,h=e.abs,d=e.fix,x=e.round,w=e.equal,i=e.isNumeric,s=e.format,r=e.number,n=e.Complex,N=e.BigNumber,O=e.Fraction,a=r;function M(e,t){if(!(this instanceof M))throw new Error("Constructor must be called with the new operator");if(null!=e&&!i(e)&&!Object(ie.j)(e))throw new TypeError("First parameter in Unit constructor must be number, BigNumber, Fraction, Complex, or undefined");if(void 0!==t&&("string"!=typeof t||""===t))throw new TypeError("Second parameter in Unit constructor must be a string");if(void 0!==t){var r=M.parse(t);this.units=r.units,this.dimensions=r.dimensions}else{this.units=[{unit:z,prefix:I.NONE,power:0}],this.dimensions=[];for(var n=0;n<q.length;n++)this.dimensions[n]=0}this.value=null!=e?this._normalize(e):null,this.fixPrefix=!1,this.skipAutomaticSimplification=!0}function E(){for(;" "===v||"\t"===v;)j()}function o(e){return"0"<=e&&e<="9"}function j(){g++,v=y.charAt(g)}function u(e){g=e,v=y.charAt(g)}function S(){var e="",t=g;if("+"===v?j():"-"===v&&(e+=v,j()),!function(e){return"0"<=e&&e<="9"||"."===e}(v))return u(t),null;if("."===v){if(e+=v,j(),!o(v))return u(t),null}else{for(;o(v);)e+=v,j();"."===v&&(e+=v,j())}for(;o(v);)e+=v,j();if("E"===v||"e"===v){var r="",n=g;if(r+=v,j(),"+"!==v&&"-"!==v||(r+=v,j()),!o(v))return u(n),e;for(e+=r;o(v);)e+=v,j()}return e}function A(){for(var e="",t=y.charCodeAt(g);48<=t&&t<=57||65<=t&&t<=90||97<=t&&t<=122;)e+=v,j(),t=y.charCodeAt(g);return(65<=(t=e.charCodeAt(0))&&t<=90||97<=t&&t<=122)&&e||null}function C(e){return v===e?(j(),e):null}function T(e){if(Object(ae.f)(D,e)){var t=D[e];return{unit:t,prefix:t.prefixes[""]}}for(var r in D)if(Object(ae.f)(D,r)&&Object(J.b)(e,r)){var n=D[r],i=e.length-r.length,a=e.substring(0,i),o=Object(ae.f)(n.prefixes,a)?n.prefixes[a]:void 0;if(void 0!==o)return{unit:n,prefix:o}}return null}function _(e){return e.equalBase(B.NONE)&&null!==e.value&&!b.predictable?e.value:e}M.prototype.type="Unit",M.prototype.isUnit=!0,M.parse=function(e,t){if(t=t||{},g=-1,v="","string"!=typeof(y=e))throw new TypeError("Invalid argument in Unit.parse, string expected");var r=new M,n=1,i=!(r.units=[]);j(),E();var a=S(),o=null;if(a){if("BigNumber"===b.number)o=new N(a);else if("Fraction"===b.number)try{o=new O(a)}catch(e){o=parseFloat(a)}else o=parseFloat(a);E(),C("*")?(n=1,i=!0):C("/")&&(n=-1,i=!0)}for(var s=[],u=1;;){for(E();"("===v;)s.push(n),u*=n,n=1,j(),E();var c=void 0;if(!v)break;var f=v;if(null===(c=A()))throw new SyntaxError('Unexpected "'+f+'" in "'+y+'" at index '+g.toString());var l=T(c);if(null===l)throw new SyntaxError('Unit "'+c+'" not found.');var p=n*u;if(E(),C("^")){E();var m=S();if(null===m)throw new SyntaxError('In "'+e+'", "^" must be followed by a floating-point number');p*=m}r.units.push({unit:l.unit,prefix:l.prefix,power:p});for(var h=0;h<q.length;h++)r.dimensions[h]+=(l.unit.dimensions[h]||0)*p;for(E();")"===v;){if(0===s.length)throw new SyntaxError('Unmatched ")" in "'+y+'" at index '+g.toString());u/=s.pop(),j(),E()}if(i=!1,C("*")?(n=1,i=!0):C("/")?(n=-1,i=!0):n=1,l.unit.base){var d=l.unit.base.key;F.auto[d]={unit:l.unit,prefix:l.prefix}}}if(E(),v)throw new SyntaxError('Could not parse: "'+e+'"');if(i)throw new SyntaxError('Trailing characters: "'+e+'"');if(0!==s.length)throw new SyntaxError('Unmatched "(" in "'+y+'"');if(0===r.units.length&&!t.allowNoUnits)throw new SyntaxError('"'+e+'" contains no units');return r.value=void 0!==o?r._normalize(o):null,r},M.prototype.clone=function(){var e=new M;e.fixPrefix=this.fixPrefix,e.skipAutomaticSimplification=this.skipAutomaticSimplification,e.value=Object(ae.a)(this.value),e.dimensions=this.dimensions.slice(0),e.units=[];for(var t=0;t<this.units.length;t++)for(var r in e.units[t]={},this.units[t])Object(ae.f)(this.units[t],r)&&(e.units[t][r]=this.units[t][r]);return e},M.prototype._isDerived=function(){return 0!==this.units.length&&(1<this.units.length||1e-15<Math.abs(this.units[0].power-1))},M.prototype._normalize=function(e){var t,r,n,i,a;if(null==e||0===this.units.length)return e;if(this._isDerived()){var o=e;a=M._getNumberConverter(Object(ie.M)(e));for(var s=0;s<this.units.length;s++)t=a(this.units[s].unit.value),i=a(this.units[s].prefix.value),n=a(this.units[s].power),o=l(o,m(l(t,i),n));return o}return t=(a=M._getNumberConverter(Object(ie.M)(e)))(this.units[0].unit.value),r=a(this.units[0].unit.offset),i=a(this.units[0].prefix.value),l(c(e,r),l(t,i))},M.prototype._denormalize=function(e,t){var r,n,i,a,o;if(null==e||0===this.units.length)return e;if(this._isDerived()){var s=e;o=M._getNumberConverter(Object(ie.M)(e));for(var u=0;u<this.units.length;u++)r=o(this.units[u].unit.value),a=o(this.units[u].prefix.value),i=o(this.units[u].power),s=p(s,m(l(r,a),i));return s}return r=(o=M._getNumberConverter(Object(ie.M)(e)))(this.units[0].unit.value),a=o(this.units[0].prefix.value),n=o(this.units[0].unit.offset),f(p(p(e,r),null==t?a:t),n)},M.isValuelessUnit=function(e){return null!==T(e)},M.prototype.hasBase=function(e){if("string"==typeof e&&(e=B[e]),!e)return!1;for(var t=0;t<q.length;t++)if(1e-12<Math.abs((this.dimensions[t]||0)-(e.dimensions[t]||0)))return!1;return!0},M.prototype.equalBase=function(e){for(var t=0;t<q.length;t++)if(1e-12<Math.abs((this.dimensions[t]||0)-(e.dimensions[t]||0)))return!1;return!0},M.prototype.equals=function(e){return this.equalBase(e)&&w(this.value,e.value)},M.prototype.multiply=function(e){for(var t=this.clone(),r=0;r<q.length;r++)t.dimensions[r]=(this.dimensions[r]||0)+(e.dimensions[r]||0);for(var n=0;n<e.units.length;n++){var i={};for(var a in e.units[n])i[a]=e.units[n][a];t.units.push(i)}if(null!==this.value||null!==e.value){var o=null===this.value?this._normalize(1):this.value,s=null===e.value?e._normalize(1):e.value;t.value=l(o,s)}else t.value=null;return t.skipAutomaticSimplification=!1,_(t)},M.prototype.divide=function(e){for(var t=this.clone(),r=0;r<q.length;r++)t.dimensions[r]=(this.dimensions[r]||0)-(e.dimensions[r]||0);for(var n=0;n<e.units.length;n++){var i={};for(var a in e.units[n])i[a]=e.units[n][a];i.power=-i.power,t.units.push(i)}if(null!==this.value||null!==e.value){var o=null===this.value?this._normalize(1):this.value,s=null===e.value?e._normalize(1):e.value;t.value=p(o,s)}else t.value=null;return t.skipAutomaticSimplification=!1,_(t)},M.prototype.pow=function(e){for(var t=this.clone(),r=0;r<q.length;r++)t.dimensions[r]=(this.dimensions[r]||0)*e;for(var n=0;n<t.units.length;n++)t.units[n].power*=e;return null!==t.value?t.value=m(t.value,e):t.value=null,t.skipAutomaticSimplification=!1,_(t)},M.prototype.abs=function(){var e=this.clone();for(var t in e.value=null!==e.value?h(e.value):null,e.units)"VA"!==e.units[t].unit.name&&"VAR"!==e.units[t].unit.name||(e.units[t].unit=D.W);return e},M.prototype.to=function(e){var t,r=null===this.value?this._normalize(1):this.value;if("string"==typeof e){if(t=M.parse(e),!this.equalBase(t))throw new Error("Units do not match ('".concat(t.toString(),"' != '").concat(this.toString(),"')"));if(null!==t.value)throw new Error("Cannot convert to a unit with a value");return t.value=Object(ae.a)(r),t.fixPrefix=!0,t.skipAutomaticSimplification=!0,t}if(Object(ie.L)(e)){if(!this.equalBase(e))throw new Error("Units do not match ('".concat(e.toString(),"' != '").concat(this.toString(),"')"));if(null!==e.value)throw new Error("Cannot convert to a unit with a value");return(t=e.clone()).value=Object(ae.a)(r),t.fixPrefix=!0,t.skipAutomaticSimplification=!0,t}throw new Error("String or Unit expected as parameter")},M.prototype.toNumber=function(e){return a(this.toNumeric(e))},M.prototype.toNumeric=function(e){var t;return(t=e?this.to(e):this.clone())._isDerived()?t._denormalize(t.value):t._denormalize(t.value,t.units[0].prefix.value)},M.prototype.toString=function(){return this.format()},M.prototype.toJSON=function(){return{mathjs:"Unit",value:this._denormalize(this.value),unit:this.formatUnits(),fixPrefix:this.fixPrefix}},M.fromJSON=function(e){var t=new M(e.value,e.unit);return t.fixPrefix=e.fixPrefix||!1,t},M.prototype.valueOf=M.prototype.toString,M.prototype.simplify=function(){var e,t,r=this.clone(),n=[];for(var i in U)if(r.hasBase(B[i])){e=i;break}if("NONE"===e)r.units=[];else if(e&&Object(ae.f)(U,e)&&(t=U[e]),t)r.units=[{unit:t.unit,prefix:t.prefix,power:1}];else{for(var a=!1,o=0;o<q.length;o++){var s=q[o];1e-12<Math.abs(r.dimensions[o]||0)&&(Object(ae.f)(U,s)?n.push({unit:U[s].unit,prefix:U[s].prefix,power:r.dimensions[o]||0}):a=!0)}n.length<r.units.length&&!a&&(r.units=n)}return r},M.prototype.toSI=function(){for(var e=this.clone(),t=[],r=0;r<q.length;r++){var n=q[r];if(1e-12<Math.abs(e.dimensions[r]||0)){if(!Object(ae.f)(F.si,n))throw new Error("Cannot express custom unit "+n+" in SI units");t.push({unit:F.si[n].unit,prefix:F.si[n].prefix,power:e.dimensions[r]||0})}}return e.units=t,e.fixPrefix=!0,e.skipAutomaticSimplification=!0,e},M.prototype.formatUnits=function(){for(var e="",t="",r=0,n=0,i=0;i<this.units.length;i++)0<this.units[i].power?(r++,e+=" "+this.units[i].prefix.name+this.units[i].unit.name,1e-15<Math.abs(this.units[i].power-1)&&(e+="^"+this.units[i].power)):this.units[i].power<0&&n++;if(0<n)for(var a=0;a<this.units.length;a++)this.units[a].power<0&&(0<r?(t+=" "+this.units[a].prefix.name+this.units[a].unit.name,1e-15<Math.abs(this.units[a].power+1)&&(t+="^"+-this.units[a].power)):(t+=" "+this.units[a].prefix.name+this.units[a].unit.name,t+="^"+this.units[a].power));e=e.substr(1),t=t.substr(1),1<r&&0<n&&(e="("+e+")"),1<n&&0<r&&(t="("+t+")");var o=e;return 0<r&&0<n&&(o+=" / "),o+=t},M.prototype.format=function(e){var t=this.skipAutomaticSimplification||null===this.value?this.clone():this.simplify(),r=!1;for(var n in void 0!==t.value&&null!==t.value&&Object(ie.j)(t.value)&&(r=Math.abs(t.value.re)<1e-14),t.units)t.units[n].unit&&("VA"===t.units[n].unit.name&&r?t.units[n].unit=D.VAR:"VAR"!==t.units[n].unit.name||r||(t.units[n].unit=D.VA));1!==t.units.length||t.fixPrefix||Math.abs(t.units[0].power-Math.round(t.units[0].power))<1e-14&&(t.units[0].prefix=t._bestPrefix());var i=t._denormalize(t.value),a=null!==t.value?s(i,e||{}):"",o=t.formatUnits();return t.value&&Object(ie.j)(t.value)&&(a="("+a+")"),0<o.length&&0<a.length&&(a+=" "),a+=o},M.prototype._bestPrefix=function(){if(1!==this.units.length)throw new Error("Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!");if(1e-14<=Math.abs(this.units[0].power-Math.round(this.units[0].power)))throw new Error("Can only compute the best prefix for single units with integer powers, like kg, s^2, N^-1, and so forth!");var e=null!==this.value?h(this.value):0,t=h(this.units[0].unit.value),r=this.units[0].prefix;if(0===e)return r;var n=this.units[0].power,i=Math.log(e/Math.pow(r.value*t,n))/Math.LN10-1.2;if(-2.200001<i&&i<1.800001)return r;i=Math.abs(i);var a=this.units[0].unit.prefixes;for(var o in a)if(Object(ae.f)(a,o)){var s=a[o];if(s.scientific){var u=Math.abs(Math.log(e/Math.pow(s.value*t,n))/Math.LN10-1.2);(u<i||u===i&&s.name.length<r.name.length)&&(r=s,i=u)}}return r};var I={NONE:{"":{name:"",value:1,scientific:!0}},SHORT:{"":{name:"",value:1,scientific:!0},da:{name:"da",value:10,scientific:!(M.prototype.splitUnit=function(e){for(var t=this.clone(),r=[],n=0;n<e.length&&(t=t.to(e[n]),n!==e.length-1);n++){var i=t.toNumeric(),a=x(i),o=new M(w(a,i)?a:d(t.toNumeric()),e[n].toString());r.push(o),t=f(t,o)}for(var s=0,u=0;u<r.length;u++)s=c(s,r[u].value);return w(s,this.value)&&(t.value=0),r.push(t),r})},h:{name:"h",value:100,scientific:!1},k:{name:"k",value:1e3,scientific:!0},M:{name:"M",value:1e6,scientific:!0},G:{name:"G",value:1e9,scientific:!0},T:{name:"T",value:1e12,scientific:!0},P:{name:"P",value:1e15,scientific:!0},E:{name:"E",value:1e18,scientific:!0},Z:{name:"Z",value:1e21,scientific:!0},Y:{name:"Y",value:1e24,scientific:!0},d:{name:"d",value:.1,scientific:!1},c:{name:"c",value:.01,scientific:!1},m:{name:"m",value:.001,scientific:!0},u:{name:"u",value:1e-6,scientific:!0},n:{name:"n",value:1e-9,scientific:!0},p:{name:"p",value:1e-12,scientific:!0},f:{name:"f",value:1e-15,scientific:!0},a:{name:"a",value:1e-18,scientific:!0},z:{name:"z",value:1e-21,scientific:!0},y:{name:"y",value:1e-24,scientific:!0}},LONG:{"":{name:"",value:1,scientific:!0},deca:{name:"deca",value:10,scientific:!1},hecto:{name:"hecto",value:100,scientific:!1},kilo:{name:"kilo",value:1e3,scientific:!0},mega:{name:"mega",value:1e6,scientific:!0},giga:{name:"giga",value:1e9,scientific:!0},tera:{name:"tera",value:1e12,scientific:!0},peta:{name:"peta",value:1e15,scientific:!0},exa:{name:"exa",value:1e18,scientific:!0},zetta:{name:"zetta",value:1e21,scientific:!0},yotta:{name:"yotta",value:1e24,scientific:!0},deci:{name:"deci",value:.1,scientific:!1},centi:{name:"centi",value:.01,scientific:!1},milli:{name:"milli",value:.001,scientific:!0},micro:{name:"micro",value:1e-6,scientific:!0},nano:{name:"nano",value:1e-9,scientific:!0},pico:{name:"pico",value:1e-12,scientific:!0},femto:{name:"femto",value:1e-15,scientific:!0},atto:{name:"atto",value:1e-18,scientific:!0},zepto:{name:"zepto",value:1e-21,scientific:!0},yocto:{name:"yocto",value:1e-24,scientific:!0}},SQUARED:{"":{name:"",value:1,scientific:!0},da:{name:"da",value:100,scientific:!1},h:{name:"h",value:1e4,scientific:!1},k:{name:"k",value:1e6,scientific:!0},M:{name:"M",value:1e12,scientific:!0},G:{name:"G",value:1e18,scientific:!0},T:{name:"T",value:1e24,scientific:!0},P:{name:"P",value:1e30,scientific:!0},E:{name:"E",value:1e36,scientific:!0},Z:{name:"Z",value:1e42,scientific:!0},Y:{name:"Y",value:1e48,scientific:!0},d:{name:"d",value:.01,scientific:!1},c:{name:"c",value:1e-4,scientific:!1},m:{name:"m",value:1e-6,scientific:!0},u:{name:"u",value:1e-12,scientific:!0},n:{name:"n",value:1e-18,scientific:!0},p:{name:"p",value:1e-24,scientific:!0},f:{name:"f",value:1e-30,scientific:!0},a:{name:"a",value:1e-36,scientific:!0},z:{name:"z",value:1e-42,scientific:!0},y:{name:"y",value:1e-48,scientific:!0}},CUBIC:{"":{name:"",value:1,scientific:!0},da:{name:"da",value:1e3,scientific:!1},h:{name:"h",value:1e6,scientific:!1},k:{name:"k",value:1e9,scientific:!0},M:{name:"M",value:1e18,scientific:!0},G:{name:"G",value:1e27,scientific:!0},T:{name:"T",value:1e36,scientific:!0},P:{name:"P",value:1e45,scientific:!0},E:{name:"E",value:1e54,scientific:!0},Z:{name:"Z",value:1e63,scientific:!0},Y:{name:"Y",value:1e72,scientific:!0},d:{name:"d",value:.001,scientific:!1},c:{name:"c",value:1e-6,scientific:!1},m:{name:"m",value:1e-9,scientific:!0},u:{name:"u",value:1e-18,scientific:!0},n:{name:"n",value:1e-27,scientific:!0},p:{name:"p",value:1e-36,scientific:!0},f:{name:"f",value:1e-45,scientific:!0},a:{name:"a",value:1e-54,scientific:!0},z:{name:"z",value:1e-63,scientific:!0},y:{name:"y",value:1e-72,scientific:!0}},BINARY_SHORT_SI:{"":{name:"",value:1,scientific:!0},k:{name:"k",value:1e3,scientific:!0},M:{name:"M",value:1e6,scientific:!0},G:{name:"G",value:1e9,scientific:!0},T:{name:"T",value:1e12,scientific:!0},P:{name:"P",value:1e15,scientific:!0},E:{name:"E",value:1e18,scientific:!0},Z:{name:"Z",value:1e21,scientific:!0},Y:{name:"Y",value:1e24,scientific:!0}},BINARY_SHORT_IEC:{"":{name:"",value:1,scientific:!0},Ki:{name:"Ki",value:1024,scientific:!0},Mi:{name:"Mi",value:Math.pow(1024,2),scientific:!0},Gi:{name:"Gi",value:Math.pow(1024,3),scientific:!0},Ti:{name:"Ti",value:Math.pow(1024,4),scientific:!0},Pi:{name:"Pi",value:Math.pow(1024,5),scientific:!0},Ei:{name:"Ei",value:Math.pow(1024,6),scientific:!0},Zi:{name:"Zi",value:Math.pow(1024,7),scientific:!0},Yi:{name:"Yi",value:Math.pow(1024,8),scientific:!0}},BINARY_LONG_SI:{"":{name:"",value:1,scientific:!0},kilo:{name:"kilo",value:1e3,scientific:!0},mega:{name:"mega",value:1e6,scientific:!0},giga:{name:"giga",value:1e9,scientific:!0},tera:{name:"tera",value:1e12,scientific:!0},peta:{name:"peta",value:1e15,scientific:!0},exa:{name:"exa",value:1e18,scientific:!0},zetta:{name:"zetta",value:1e21,scientific:!0},yotta:{name:"yotta",value:1e24,scientific:!0}},BINARY_LONG_IEC:{"":{name:"",value:1,scientific:!0},kibi:{name:"kibi",value:1024,scientific:!0},mebi:{name:"mebi",value:Math.pow(1024,2),scientific:!0},gibi:{name:"gibi",value:Math.pow(1024,3),scientific:!0},tebi:{name:"tebi",value:Math.pow(1024,4),scientific:!0},pebi:{name:"pebi",value:Math.pow(1024,5),scientific:!0},exi:{name:"exi",value:Math.pow(1024,6),scientific:!0},zebi:{name:"zebi",value:Math.pow(1024,7),scientific:!0},yobi:{name:"yobi",value:Math.pow(1024,8),scientific:!0}},BTU:{"":{name:"",value:1,scientific:!0},MM:{name:"MM",value:1e6,scientific:!0}}};I.SHORTLONG=Os({},I.SHORT,I.LONG),I.BINARY_SHORT=Os({},I.BINARY_SHORT_SI,I.BINARY_SHORT_IEC),I.BINARY_LONG=Os({},I.BINARY_LONG_SI,I.BINARY_LONG_IEC);var q=["MASS","LENGTH","TIME","CURRENT","TEMPERATURE","LUMINOUS_INTENSITY","AMOUNT_OF_SUBSTANCE","ANGLE","BIT"],B={NONE:{dimensions:[0,0,0,0,0,0,0,0,0]},MASS:{dimensions:[1,0,0,0,0,0,0,0,0]},LENGTH:{dimensions:[0,1,0,0,0,0,0,0,0]},TIME:{dimensions:[0,0,1,0,0,0,0,0,0]},CURRENT:{dimensions:[0,0,0,1,0,0,0,0,0]},TEMPERATURE:{dimensions:[0,0,0,0,1,0,0,0,0]},LUMINOUS_INTENSITY:{dimensions:[0,0,0,0,0,1,0,0,0]},AMOUNT_OF_SUBSTANCE:{dimensions:[0,0,0,0,0,0,1,0,0]},FORCE:{dimensions:[1,1,-2,0,0,0,0,0,0]},SURFACE:{dimensions:[0,2,0,0,0,0,0,0,0]},VOLUME:{dimensions:[0,3,0,0,0,0,0,0,0]},ENERGY:{dimensions:[1,2,-2,0,0,0,0,0,0]},POWER:{dimensions:[1,2,-3,0,0,0,0,0,0]},PRESSURE:{dimensions:[1,-1,-2,0,0,0,0,0,0]},ELECTRIC_CHARGE:{dimensions:[0,0,1,1,0,0,0,0,0]},ELECTRIC_CAPACITANCE:{dimensions:[-1,-2,4,2,0,0,0,0,0]},ELECTRIC_POTENTIAL:{dimensions:[1,2,-3,-1,0,0,0,0,0]},ELECTRIC_RESISTANCE:{dimensions:[1,2,-3,-2,0,0,0,0,0]},ELECTRIC_INDUCTANCE:{dimensions:[1,2,-2,-2,0,0,0,0,0]},ELECTRIC_CONDUCTANCE:{dimensions:[-1,-2,3,2,0,0,0,0,0]},MAGNETIC_FLUX:{dimensions:[1,2,-2,-1,0,0,0,0,0]},MAGNETIC_FLUX_DENSITY:{dimensions:[1,0,-2,-1,0,0,0,0,0]},FREQUENCY:{dimensions:[0,0,-1,0,0,0,0,0,0]},ANGLE:{dimensions:[0,0,0,0,0,0,0,1,0]},BIT:{dimensions:[0,0,0,0,0,0,0,0,1]}};for(var k in B)B[k].key=k;var z={name:"",base:{},value:1,offset:0,dimensions:q.map(function(e){return 0})},D={meter:{name:"meter",base:B.LENGTH,prefixes:I.LONG,value:1,offset:0},inch:{name:"inch",base:B.LENGTH,prefixes:I.NONE,value:.0254,offset:0},foot:{name:"foot",base:B.LENGTH,prefixes:I.NONE,value:.3048,offset:0},yard:{name:"yard",base:B.LENGTH,prefixes:I.NONE,value:.9144,offset:0},mile:{name:"mile",base:B.LENGTH,prefixes:I.NONE,value:1609.344,offset:0},link:{name:"link",base:B.LENGTH,prefixes:I.NONE,value:.201168,offset:0},rod:{name:"rod",base:B.LENGTH,prefixes:I.NONE,value:5.0292,offset:0},chain:{name:"chain",base:B.LENGTH,prefixes:I.NONE,value:20.1168,offset:0},angstrom:{name:"angstrom",base:B.LENGTH,prefixes:I.NONE,value:1e-10,offset:0},m:{name:"m",base:B.LENGTH,prefixes:I.SHORT,value:1,offset:0},in:{name:"in",base:B.LENGTH,prefixes:I.NONE,value:.0254,offset:0},ft:{name:"ft",base:B.LENGTH,prefixes:I.NONE,value:.3048,offset:0},yd:{name:"yd",base:B.LENGTH,prefixes:I.NONE,value:.9144,offset:0},mi:{name:"mi",base:B.LENGTH,prefixes:I.NONE,value:1609.344,offset:0},li:{name:"li",base:B.LENGTH,prefixes:I.NONE,value:.201168,offset:0},rd:{name:"rd",base:B.LENGTH,prefixes:I.NONE,value:5.02921,offset:0},ch:{name:"ch",base:B.LENGTH,prefixes:I.NONE,value:20.1168,offset:0},mil:{name:"mil",base:B.LENGTH,prefixes:I.NONE,value:254e-7,offset:0},m2:{name:"m2",base:B.SURFACE,prefixes:I.SQUARED,value:1,offset:0},sqin:{name:"sqin",base:B.SURFACE,prefixes:I.NONE,value:64516e-8,offset:0},sqft:{name:"sqft",base:B.SURFACE,prefixes:I.NONE,value:.09290304,offset:0},sqyd:{name:"sqyd",base:B.SURFACE,prefixes:I.NONE,value:.83612736,offset:0},sqmi:{name:"sqmi",base:B.SURFACE,prefixes:I.NONE,value:2589988.110336,offset:0},sqrd:{name:"sqrd",base:B.SURFACE,prefixes:I.NONE,value:25.29295,offset:0},sqch:{name:"sqch",base:B.SURFACE,prefixes:I.NONE,value:404.6873,offset:0},sqmil:{name:"sqmil",base:B.SURFACE,prefixes:I.NONE,value:6.4516e-10,offset:0},acre:{name:"acre",base:B.SURFACE,prefixes:I.NONE,value:4046.86,offset:0},hectare:{name:"hectare",base:B.SURFACE,prefixes:I.NONE,value:1e4,offset:0},m3:{name:"m3",base:B.VOLUME,prefixes:I.CUBIC,value:1,offset:0},L:{name:"L",base:B.VOLUME,prefixes:I.SHORT,value:.001,offset:0},l:{name:"l",base:B.VOLUME,prefixes:I.SHORT,value:.001,offset:0},litre:{name:"litre",base:B.VOLUME,prefixes:I.LONG,value:.001,offset:0},cuin:{name:"cuin",base:B.VOLUME,prefixes:I.NONE,value:16387064e-12,offset:0},cuft:{name:"cuft",base:B.VOLUME,prefixes:I.NONE,value:.028316846592,offset:0},cuyd:{name:"cuyd",base:B.VOLUME,prefixes:I.NONE,value:.764554857984,offset:0},teaspoon:{name:"teaspoon",base:B.VOLUME,prefixes:I.NONE,value:5e-6,offset:0},tablespoon:{name:"tablespoon",base:B.VOLUME,prefixes:I.NONE,value:15e-6,offset:0},drop:{name:"drop",base:B.VOLUME,prefixes:I.NONE,value:5e-8,offset:0},gtt:{name:"gtt",base:B.VOLUME,prefixes:I.NONE,value:5e-8,offset:0},minim:{name:"minim",base:B.VOLUME,prefixes:I.NONE,value:6.161152e-8,offset:0},fluiddram:{name:"fluiddram",base:B.VOLUME,prefixes:I.NONE,value:36966911e-13,offset:0},fluidounce:{name:"fluidounce",base:B.VOLUME,prefixes:I.NONE,value:2957353e-11,offset:0},gill:{name:"gill",base:B.VOLUME,prefixes:I.NONE,value:.0001182941,offset:0},cc:{name:"cc",base:B.VOLUME,prefixes:I.NONE,value:1e-6,offset:0},cup:{name:"cup",base:B.VOLUME,prefixes:I.NONE,value:.0002365882,offset:0},pint:{name:"pint",base:B.VOLUME,prefixes:I.NONE,value:.0004731765,offset:0},quart:{name:"quart",base:B.VOLUME,prefixes:I.NONE,value:.0009463529,offset:0},gallon:{name:"gallon",base:B.VOLUME,prefixes:I.NONE,value:.003785412,offset:0},beerbarrel:{name:"beerbarrel",base:B.VOLUME,prefixes:I.NONE,value:.1173478,offset:0},oilbarrel:{name:"oilbarrel",base:B.VOLUME,prefixes:I.NONE,value:.1589873,offset:0},hogshead:{name:"hogshead",base:B.VOLUME,prefixes:I.NONE,value:.238481,offset:0},fldr:{name:"fldr",base:B.VOLUME,prefixes:I.NONE,value:36966911e-13,offset:0},floz:{name:"floz",base:B.VOLUME,prefixes:I.NONE,value:2957353e-11,offset:0},gi:{name:"gi",base:B.VOLUME,prefixes:I.NONE,value:.0001182941,offset:0},cp:{name:"cp",base:B.VOLUME,prefixes:I.NONE,value:.0002365882,offset:0},pt:{name:"pt",base:B.VOLUME,prefixes:I.NONE,value:.0004731765,offset:0},qt:{name:"qt",base:B.VOLUME,prefixes:I.NONE,value:.0009463529,offset:0},gal:{name:"gal",base:B.VOLUME,prefixes:I.NONE,value:.003785412,offset:0},bbl:{name:"bbl",base:B.VOLUME,prefixes:I.NONE,value:.1173478,offset:0},obl:{name:"obl",base:B.VOLUME,prefixes:I.NONE,value:.1589873,offset:0},g:{name:"g",base:B.MASS,prefixes:I.SHORT,value:.001,offset:0},gram:{name:"gram",base:B.MASS,prefixes:I.LONG,value:.001,offset:0},ton:{name:"ton",base:B.MASS,prefixes:I.SHORT,value:907.18474,offset:0},t:{name:"t",base:B.MASS,prefixes:I.SHORT,value:1e3,offset:0},tonne:{name:"tonne",base:B.MASS,prefixes:I.LONG,value:1e3,offset:0},grain:{name:"grain",base:B.MASS,prefixes:I.NONE,value:6479891e-11,offset:0},dram:{name:"dram",base:B.MASS,prefixes:I.NONE,value:.0017718451953125,offset:0},ounce:{name:"ounce",base:B.MASS,prefixes:I.NONE,value:.028349523125,offset:0},poundmass:{name:"poundmass",base:B.MASS,prefixes:I.NONE,value:.45359237,offset:0},hundredweight:{name:"hundredweight",base:B.MASS,prefixes:I.NONE,value:45.359237,offset:0},stick:{name:"stick",base:B.MASS,prefixes:I.NONE,value:.115,offset:0},stone:{name:"stone",base:B.MASS,prefixes:I.NONE,value:6.35029318,offset:0},gr:{name:"gr",base:B.MASS,prefixes:I.NONE,value:6479891e-11,offset:0},dr:{name:"dr",base:B.MASS,prefixes:I.NONE,value:.0017718451953125,offset:0},oz:{name:"oz",base:B.MASS,prefixes:I.NONE,value:.028349523125,offset:0},lbm:{name:"lbm",base:B.MASS,prefixes:I.NONE,value:.45359237,offset:0},cwt:{name:"cwt",base:B.MASS,prefixes:I.NONE,value:45.359237,offset:0},s:{name:"s",base:B.TIME,prefixes:I.SHORT,value:1,offset:0},min:{name:"min",base:B.TIME,prefixes:I.NONE,value:60,offset:0},h:{name:"h",base:B.TIME,prefixes:I.NONE,value:3600,offset:0},second:{name:"second",base:B.TIME,prefixes:I.LONG,value:1,offset:0},sec:{name:"sec",base:B.TIME,prefixes:I.LONG,value:1,offset:0},minute:{name:"minute",base:B.TIME,prefixes:I.NONE,value:60,offset:0},hour:{name:"hour",base:B.TIME,prefixes:I.NONE,value:3600,offset:0},day:{name:"day",base:B.TIME,prefixes:I.NONE,value:86400,offset:0},week:{name:"week",base:B.TIME,prefixes:I.NONE,value:604800,offset:0},month:{name:"month",base:B.TIME,prefixes:I.NONE,value:2629800,offset:0},year:{name:"year",base:B.TIME,prefixes:I.NONE,value:31557600,offset:0},decade:{name:"decade",base:B.TIME,prefixes:I.NONE,value:315576e3,offset:0},century:{name:"century",base:B.TIME,prefixes:I.NONE,value:315576e4,offset:0},millennium:{name:"millennium",base:B.TIME,prefixes:I.NONE,value:315576e5,offset:0},hertz:{name:"Hertz",base:B.FREQUENCY,prefixes:I.LONG,value:1,offset:0,reciprocal:!0},Hz:{name:"Hz",base:B.FREQUENCY,prefixes:I.SHORT,value:1,offset:0,reciprocal:!0},rad:{name:"rad",base:B.ANGLE,prefixes:I.SHORT,value:1,offset:0},radian:{name:"radian",base:B.ANGLE,prefixes:I.LONG,value:1,offset:0},deg:{name:"deg",base:B.ANGLE,prefixes:I.SHORT,value:null,offset:0},degree:{name:"degree",base:B.ANGLE,prefixes:I.LONG,value:null,offset:0},grad:{name:"grad",base:B.ANGLE,prefixes:I.SHORT,value:null,offset:0},gradian:{name:"gradian",base:B.ANGLE,prefixes:I.LONG,value:null,offset:0},cycle:{name:"cycle",base:B.ANGLE,prefixes:I.NONE,value:null,offset:0},arcsec:{name:"arcsec",base:B.ANGLE,prefixes:I.NONE,value:null,offset:0},arcmin:{name:"arcmin",base:B.ANGLE,prefixes:I.NONE,value:null,offset:0},A:{name:"A",base:B.CURRENT,prefixes:I.SHORT,value:1,offset:0},ampere:{name:"ampere",base:B.CURRENT,prefixes:I.LONG,value:1,offset:0},K:{name:"K",base:B.TEMPERATURE,prefixes:I.NONE,value:1,offset:0},degC:{name:"degC",base:B.TEMPERATURE,prefixes:I.NONE,value:1,offset:273.15},degF:{name:"degF",base:B.TEMPERATURE,prefixes:I.NONE,value:1/1.8,offset:459.67},degR:{name:"degR",base:B.TEMPERATURE,prefixes:I.NONE,value:1/1.8,offset:0},kelvin:{name:"kelvin",base:B.TEMPERATURE,prefixes:I.NONE,value:1,offset:0},celsius:{name:"celsius",base:B.TEMPERATURE,prefixes:I.NONE,value:1,offset:273.15},fahrenheit:{name:"fahrenheit",base:B.TEMPERATURE,prefixes:I.NONE,value:1/1.8,offset:459.67},rankine:{name:"rankine",base:B.TEMPERATURE,prefixes:I.NONE,value:1/1.8,offset:0},mol:{name:"mol",base:B.AMOUNT_OF_SUBSTANCE,prefixes:I.SHORT,value:1,offset:0},mole:{name:"mole",base:B.AMOUNT_OF_SUBSTANCE,prefixes:I.LONG,value:1,offset:0},cd:{name:"cd",base:B.LUMINOUS_INTENSITY,prefixes:I.SHORT,value:1,offset:0},candela:{name:"candela",base:B.LUMINOUS_INTENSITY,prefixes:I.LONG,value:1,offset:0},N:{name:"N",base:B.FORCE,prefixes:I.SHORT,value:1,offset:0},newton:{name:"newton",base:B.FORCE,prefixes:I.LONG,value:1,offset:0},dyn:{name:"dyn",base:B.FORCE,prefixes:I.SHORT,value:1e-5,offset:0},dyne:{name:"dyne",base:B.FORCE,prefixes:I.LONG,value:1e-5,offset:0},lbf:{name:"lbf",base:B.FORCE,prefixes:I.NONE,value:4.4482216152605,offset:0},poundforce:{name:"poundforce",base:B.FORCE,prefixes:I.NONE,value:4.4482216152605,offset:0},kip:{name:"kip",base:B.FORCE,prefixes:I.LONG,value:4448.2216,offset:0},J:{name:"J",base:B.ENERGY,prefixes:I.SHORT,value:1,offset:0},joule:{name:"joule",base:B.ENERGY,prefixes:I.SHORT,value:1,offset:0},erg:{name:"erg",base:B.ENERGY,prefixes:I.NONE,value:1e-7,offset:0},Wh:{name:"Wh",base:B.ENERGY,prefixes:I.SHORT,value:3600,offset:0},BTU:{name:"BTU",base:B.ENERGY,prefixes:I.BTU,value:1055.05585262,offset:0},eV:{name:"eV",base:B.ENERGY,prefixes:I.SHORT,value:1602176565e-28,offset:0},electronvolt:{name:"electronvolt",base:B.ENERGY,prefixes:I.LONG,value:1602176565e-28,offset:0},W:{name:"W",base:B.POWER,prefixes:I.SHORT,value:1,offset:0},watt:{name:"watt",base:B.POWER,prefixes:I.LONG,value:1,offset:0},hp:{name:"hp",base:B.POWER,prefixes:I.NONE,value:745.6998715386,offset:0},VAR:{name:"VAR",base:B.POWER,prefixes:I.SHORT,value:n.I,offset:0},VA:{name:"VA",base:B.POWER,prefixes:I.SHORT,value:1,offset:0},Pa:{name:"Pa",base:B.PRESSURE,prefixes:I.SHORT,value:1,offset:0},psi:{name:"psi",base:B.PRESSURE,prefixes:I.NONE,value:6894.75729276459,offset:0},atm:{name:"atm",base:B.PRESSURE,prefixes:I.NONE,value:101325,offset:0},bar:{name:"bar",base:B.PRESSURE,prefixes:I.SHORTLONG,value:1e5,offset:0},torr:{name:"torr",base:B.PRESSURE,prefixes:I.NONE,value:133.322,offset:0},mmHg:{name:"mmHg",base:B.PRESSURE,prefixes:I.NONE,value:133.322,offset:0},mmH2O:{name:"mmH2O",base:B.PRESSURE,prefixes:I.NONE,value:9.80665,offset:0},cmH2O:{name:"cmH2O",base:B.PRESSURE,prefixes:I.NONE,value:98.0665,offset:0},coulomb:{name:"coulomb",base:B.ELECTRIC_CHARGE,prefixes:I.LONG,value:1,offset:0},C:{name:"C",base:B.ELECTRIC_CHARGE,prefixes:I.SHORT,value:1,offset:0},farad:{name:"farad",base:B.ELECTRIC_CAPACITANCE,prefixes:I.LONG,value:1,offset:0},F:{name:"F",base:B.ELECTRIC_CAPACITANCE,prefixes:I.SHORT,value:1,offset:0},volt:{name:"volt",base:B.ELECTRIC_POTENTIAL,prefixes:I.LONG,value:1,offset:0},V:{name:"V",base:B.ELECTRIC_POTENTIAL,prefixes:I.SHORT,value:1,offset:0},ohm:{name:"ohm",base:B.ELECTRIC_RESISTANCE,prefixes:I.SHORTLONG,value:1,offset:0},henry:{name:"henry",base:B.ELECTRIC_INDUCTANCE,prefixes:I.LONG,value:1,offset:0},H:{name:"H",base:B.ELECTRIC_INDUCTANCE,prefixes:I.SHORT,value:1,offset:0},siemens:{name:"siemens",base:B.ELECTRIC_CONDUCTANCE,prefixes:I.LONG,value:1,offset:0},S:{name:"S",base:B.ELECTRIC_CONDUCTANCE,prefixes:I.SHORT,value:1,offset:0},weber:{name:"weber",base:B.MAGNETIC_FLUX,prefixes:I.LONG,value:1,offset:0},Wb:{name:"Wb",base:B.MAGNETIC_FLUX,prefixes:I.SHORT,value:1,offset:0},tesla:{name:"tesla",base:B.MAGNETIC_FLUX_DENSITY,prefixes:I.LONG,value:1,offset:0},T:{name:"T",base:B.MAGNETIC_FLUX_DENSITY,prefixes:I.SHORT,value:1,offset:0},b:{name:"b",base:B.BIT,prefixes:I.BINARY_SHORT,value:1,offset:0},bits:{name:"bits",base:B.BIT,prefixes:I.BINARY_LONG,value:1,offset:0},B:{name:"B",base:B.BIT,prefixes:I.BINARY_SHORT,value:8,offset:0},bytes:{name:"bytes",base:B.BIT,prefixes:I.BINARY_LONG,value:8,offset:0}},R={meters:"meter",inches:"inch",feet:"foot",yards:"yard",miles:"mile",links:"link",rods:"rod",chains:"chain",angstroms:"angstrom",lt:"l",litres:"litre",liter:"litre",liters:"litre",teaspoons:"teaspoon",tablespoons:"tablespoon",minims:"minim",fluiddrams:"fluiddram",fluidounces:"fluidounce",gills:"gill",cups:"cup",pints:"pint",quarts:"quart",gallons:"gallon",beerbarrels:"beerbarrel",oilbarrels:"oilbarrel",hogsheads:"hogshead",gtts:"gtt",grams:"gram",tons:"ton",tonnes:"tonne",grains:"grain",drams:"dram",ounces:"ounce",poundmasses:"poundmass",hundredweights:"hundredweight",sticks:"stick",lb:"lbm",lbs:"lbm",kips:"kip",acres:"acre",hectares:"hectare",sqfeet:"sqft",sqyard:"sqyd",sqmile:"sqmi",sqmiles:"sqmi",mmhg:"mmHg",mmh2o:"mmH2O",cmh2o:"cmH2O",seconds:"second",secs:"second",minutes:"minute",mins:"minute",hours:"hour",hr:"hour",hrs:"hour",days:"day",weeks:"week",months:"month",years:"year",decades:"decade",centuries:"century",millennia:"millennium",hertz:"hertz",radians:"radian",degrees:"degree",gradians:"gradian",cycles:"cycle",arcsecond:"arcsec",arcseconds:"arcsec",arcminute:"arcmin",arcminutes:"arcmin",BTUs:"BTU",watts:"watt",joules:"joule",amperes:"ampere",coulombs:"coulomb",volts:"volt",ohms:"ohm",farads:"farad",webers:"weber",teslas:"tesla",electronvolts:"electronvolt",moles:"mole",bit:"bits",byte:"bytes"};function P(e){if("BigNumber"===e.number){var t=bs(N);D.rad.value=new N(1),D.deg.value=t.div(180),D.grad.value=t.div(200),D.cycle.value=t.times(2),D.arcsec.value=t.div(648e3),D.arcmin.value=t.div(10800)}else D.rad.value=1,D.deg.value=Math.PI/180,D.grad.value=Math.PI/200,D.cycle.value=2*Math.PI,D.arcsec.value=Math.PI/648e3,D.arcmin.value=Math.PI/10800;D.radian.value=D.rad.value,D.degree.value=D.deg.value,D.gradian.value=D.grad.value}P(b),t&&t("config",function(e,t){e.number!==t.number&&P(e)});var F={si:{NONE:{unit:z,prefix:I.NONE[""]},LENGTH:{unit:D.m,prefix:I.SHORT[""]},MASS:{unit:D.g,prefix:I.SHORT.k},TIME:{unit:D.s,prefix:I.SHORT[""]},CURRENT:{unit:D.A,prefix:I.SHORT[""]},TEMPERATURE:{unit:D.K,prefix:I.SHORT[""]},LUMINOUS_INTENSITY:{unit:D.cd,prefix:I.SHORT[""]},AMOUNT_OF_SUBSTANCE:{unit:D.mol,prefix:I.SHORT[""]},ANGLE:{unit:D.rad,prefix:I.SHORT[""]},BIT:{unit:D.bits,prefix:I.SHORT[""]},FORCE:{unit:D.N,prefix:I.SHORT[""]},ENERGY:{unit:D.J,prefix:I.SHORT[""]},POWER:{unit:D.W,prefix:I.SHORT[""]},PRESSURE:{unit:D.Pa,prefix:I.SHORT[""]},ELECTRIC_CHARGE:{unit:D.C,prefix:I.SHORT[""]},ELECTRIC_CAPACITANCE:{unit:D.F,prefix:I.SHORT[""]},ELECTRIC_POTENTIAL:{unit:D.V,prefix:I.SHORT[""]},ELECTRIC_RESISTANCE:{unit:D.ohm,prefix:I.SHORT[""]},ELECTRIC_INDUCTANCE:{unit:D.H,prefix:I.SHORT[""]},ELECTRIC_CONDUCTANCE:{unit:D.S,prefix:I.SHORT[""]},MAGNETIC_FLUX:{unit:D.Wb,prefix:I.SHORT[""]},MAGNETIC_FLUX_DENSITY:{unit:D.T,prefix:I.SHORT[""]},FREQUENCY:{unit:D.Hz,prefix:I.SHORT[""]}}};F.cgs=JSON.parse(JSON.stringify(F.si)),F.cgs.LENGTH={unit:D.m,prefix:I.SHORT.c},F.cgs.MASS={unit:D.g,prefix:I.SHORT[""]},F.cgs.FORCE={unit:D.dyn,prefix:I.SHORT[""]},F.cgs.ENERGY={unit:D.erg,prefix:I.NONE[""]},F.us=JSON.parse(JSON.stringify(F.si)),F.us.LENGTH={unit:D.ft,prefix:I.NONE[""]},F.us.MASS={unit:D.lbm,prefix:I.NONE[""]},F.us.TEMPERATURE={unit:D.degF,prefix:I.NONE[""]},F.us.FORCE={unit:D.lbf,prefix:I.NONE[""]},F.us.ENERGY={unit:D.BTU,prefix:I.BTU[""]},F.us.POWER={unit:D.hp,prefix:I.NONE[""]},F.us.PRESSURE={unit:D.psi,prefix:I.NONE[""]},F.auto=JSON.parse(JSON.stringify(F.si));var U=F.auto;for(var L in M.setUnitSystem=function(e){if(!Object(ae.f)(F,e))throw new Error("Unit system "+e+" does not exist. Choices are: "+Object.keys(F).join(", "));U=F[e]},M.getUnitSystem=function(){for(var e in F)if(F[e]===U)return e},M.typeConverters={BigNumber:function(e){return new N(e+"")},Fraction:function(e){return new O(e)},Complex:function(e){return e},number:function(e){return e}},M._getNumberConverter=function(e){if(!M.typeConverters[e])throw new TypeError('Unsupported type "'+e+'"');return M.typeConverters[e]},D){var H=D[L];H.dimensions=H.base.dimensions}for(var $ in R)if(Object(ae.f)(R,$)){var G=D[R[$]],Z={};for(var V in G)Object(ae.f)(G,V)&&(Z[V]=G[V]);Z.name=$,D[$]=Z}return M.createUnit=function(e,t){if("object"!==Ns(e))throw new TypeError("createUnit expects first parameter to be of type 'Object'");if(t&&t.override)for(var r in e)if(Object(ae.f)(e,r)&&M.deleteUnit(r),e[r].aliases)for(var n=0;n<e[r].aliases.length;n++)M.deleteUnit(e[r].aliases[n]);var i;for(var a in e)Object(ae.f)(e,a)&&(i=M.createUnitSingle(a,e[a]));return i},M.createUnitSingle=function(t,e,r){if(null==e&&(e={}),"string"!=typeof t)throw new TypeError("createUnitSingle expects first parameter to be of type 'string'");if(Object(ae.f)(D,t))throw new Error('Cannot create unit "'+t+'": a unit with that name already exists');!function(e){for(var t=0;t<e.length;t++){var r=e.charAt(t),n=function(e){return/^[a-zA-Z]$/.test(e)};if(0===t&&!n(r))throw new Error('Invalid unit name (must begin with alpha character): "'+e+'"');if(0<t&&!(n(r)||"0"<=(i=r)&&i<="9"))throw new Error('Invalid unit name (only alphanumeric characters are allowed): "'+e+'"')}var i}(t);var n,i,a=null,o=[],s=0;if(e&&"Unit"===e.type)a=e.clone();else if("string"==typeof e)""!==e&&(n=e);else{if("object"!==Ns(e))throw new TypeError('Cannot create unit "'+t+'" from "'+e.toString()+'": expecting "string" or "Unit" or "Object"');n=e.definition,i=e.prefixes,s=e.offset,e.aliases&&(o=e.aliases.valueOf())}if(o)for(var u=0;u<o.length;u++)if(Object(ae.f)(D,o[u]))throw new Error('Cannot create alias "'+o[u]+'": a unit with that name already exists');if(n&&"string"==typeof n&&!a)try{a=M.parse(n,{allowNoUnits:!0})}catch(e){throw e.message='Could not create unit "'+t+'" from "'+n+'": '+e.message,e}else n&&"Unit"===n.type&&(a=n.clone());o=o||[],s=s||0,i=i&&i.toUpperCase&&I[i.toUpperCase()]||I.NONE;var c={};if(a){var f=!(c={name:t,value:a.value,dimensions:a.dimensions.slice(0),prefixes:i,offset:s});for(var l in B)if(Object(ae.f)(B,l)){for(var p=!0,m=0;m<q.length;m++)if(1e-12<Math.abs((c.dimensions[m]||0)-(B[l].dimensions[m]||0))){p=!1;break}if(p){f=!0,c.base=B[l];break}}if(!f){var h=t+"_STUFF",d={dimensions:a.dimensions.slice(0)};d.key=h,B[h]=d,U[h]={unit:c,prefix:I.NONE[""]},c.base=B[h]}}else{var y=t+"_STUFF";if(0<=q.indexOf(y))throw new Error('Cannot create new base unit "'+t+'": a base unit with that name already exists (and cannot be overridden)');for(var g in q.push(y),B)Object(ae.f)(B,g)&&(B[g].dimensions[q.length-1]=0);for(var v={dimensions:[]},b=0;b<q.length;b++)v.dimensions[b]=0;v.dimensions[q.length-1]=1,v.key=y,B[y]=v,c={name:t,value:1,dimensions:B[y].dimensions.slice(0),prefixes:i,offset:s,base:B[y]},U[y]={unit:c,prefix:I.NONE[""]}}M.UNITS[t]=c;for(var x=0;x<o.length;x++){var w=o[x],N={};for(var O in c)Object(ae.f)(c,O)&&(N[O]=c[O]);N.name=w,M.UNITS[w]=N}return new M(null,t)},M.deleteUnit=function(e){delete M.UNITS[e]},M.PREFIXES=I,M.BASE_DIMENSIONS=q,M.BASE_UNITS=B,M.UNIT_SYSTEMS=F,M.UNITS=D,M},{isClass:!0}),js=["typed","Unit"],Ss=Object(s.a)("unit",js,function(e){var t=e.typed,r=e.Unit,n=t("unit",{Unit:function(e){return e.clone()},string:function(e){return r.isValuelessUnit(e)?new r(null,e):r.parse(e,{allowNoUnits:!0})},"number | BigNumber | Fraction | Complex, string":function(e,t){return new r(e,t)},"Array | Matrix":function(e){return oe(e,n)}});return n}),As=["typed","SparseMatrix"],Cs=Object(s.a)("sparse",As,function(e){var t=e.typed,r=e.SparseMatrix;return t("sparse",{"":function(){return new r([])},string:function(e){return new r([],e)},"Array | Matrix":function(e){return new r(e)},"Array | Matrix, string":function(e,t){return new r(e,t)}})}),Ts="createUnit",_s=["typed","Unit"],Is=Object(s.a)(Ts,_s,function(e){var t=e.typed,i=e.Unit;return t(Ts,{"Object, Object":function(e,t){return i.createUnit(e,t)},Object:function(e){return i.createUnit(e,{})},"string, Unit | string | Object, Object":function(e,t,r){var n={};return n[e]=t,i.createUnit(n,r)},"string, Unit | string | Object":function(e,t){var r={};return r[e]=t,i.createUnit(r,{})},string:function(e){var t={};return t[e]={},i.createUnit(t,{})}})}),qs=["typed","config","Complex"],Bs=Object(s.a)("acos",qs,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("acos",{number:function(e){return-1<=e&&e<=1||r.predictable?Math.acos(e):new n(e,0).acos()},Complex:function(e){return e.acos()},BigNumber:function(e){return e.acos()},"Array | Matrix":function(e){return oe(e,i)}});return i}),ks="number";function zs(e){return Object(j.a)(e)}function Ds(e){return Math.atan(1/e)}function Rs(e){return isFinite(e)?(Math.log((e+1)/e)+Math.log(e/(e-1)))/2:0}function Ps(e){return Math.asin(1/e)}function Fs(e){var t=1/e;return Math.log(t+Math.sqrt(t*t+1))}function Us(e){return Math.acos(1/e)}function Ls(e){var t=1/e,r=Math.sqrt(t*t-1);return Math.log(r+t)}function Hs(e){return Object(j.b)(e)}function $s(e){return Object(j.c)(e)}function Gs(e){return 1/Math.tan(e)}function Zs(e){var t=Math.exp(2*e);return(t+1)/(t-1)}function Vs(e){return 1/Math.sin(e)}function Js(e){return 0===e?Number.POSITIVE_INFINITY:Math.abs(2/(Math.exp(e)-Math.exp(-e)))*Object(j.n)(e)}function Ws(e){return 1/Math.cos(e)}function Ys(e){return 2/(Math.exp(e)+Math.exp(-e))}function Xs(e){return Object(j.o)(e)}Xs.signature=Ys.signature=Ws.signature=Js.signature=Vs.signature=Zs.signature=Gs.signature=$s.signature=Hs.signature=Ls.signature=Us.signature=Fs.signature=Ps.signature=Rs.signature=Ds.signature=zs.signature=ks;var Qs=["typed","config","Complex"],Ks=Object(s.a)("acosh",Qs,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("acosh",{number:function(e){return 1<=e||r.predictable?zs(e):e<=-1?new n(Math.log(Math.sqrt(e*e-1)-e),Math.PI):new n(e,0).acosh()},Complex:function(e){return e.acosh()},BigNumber:function(e){return e.acosh()},"Array | Matrix":function(e){return oe(e,i)}});return i}),eu=["typed","BigNumber"],tu=Object(s.a)("acot",eu,function(e){var t=e.typed,r=e.BigNumber,n=t("acot",{number:Ds,Complex:function(e){return e.acot()},BigNumber:function(e){return new r(1).div(e).atan()},"Array | Matrix":function(e){return oe(e,n)}});return n}),ru=["typed","config","Complex","BigNumber"],nu=Object(s.a)("acoth",ru,function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber,a=t("acoth",{number:function(e){return 1<=e||e<=-1||r.predictable?Rs(e):new n(e,0).acoth()},Complex:function(e){return e.acoth()},BigNumber:function(e){return new i(1).div(e).atanh()},"Array | Matrix":function(e){return oe(e,a)}});return a}),iu=["typed","config","Complex","BigNumber"],au=Object(s.a)("acsc",iu,function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber,a=t("acsc",{number:function(e){return e<=-1||1<=e||r.predictable?Ps(e):new n(e,0).acsc()},Complex:function(e){return e.acsc()},BigNumber:function(e){return new i(1).div(e).asin()},"Array | Matrix":function(e){return oe(e,a)}});return a}),ou=["typed","BigNumber"],su=Object(s.a)("acsch",ou,function(e){var t=e.typed,r=e.BigNumber,n=t("acsch",{number:Fs,Complex:function(e){return e.acsch()},BigNumber:function(e){return new r(1).div(e).asinh()},"Array | Matrix":function(e){return oe(e,n)}});return n}),uu=["typed","config","Complex","BigNumber"],cu=Object(s.a)("asec",uu,function(e){var t=e.typed,r=e.config,n=e.Complex,i=e.BigNumber,a=t("asec",{number:function(e){return e<=-1||1<=e||r.predictable?Us(e):new n(e,0).asec()},Complex:function(e){return e.asec()},BigNumber:function(e){return new i(1).div(e).acos()},"Array | Matrix":function(e){return oe(e,a)}});return a}),fu=["typed","config","Complex","BigNumber"],lu=Object(s.a)("asech",fu,function(e){var t=e.typed,n=e.config,i=e.Complex,r=e.BigNumber,a=t("asech",{number:function(e){if(e<=1&&-1<=e||n.predictable){var t=1/e;if(0<t||n.predictable)return Ls(e);var r=Math.sqrt(t*t-1);return new i(Math.log(r-t),Math.PI)}return new i(e,0).asech()},Complex:function(e){return e.asech()},BigNumber:function(e){return new r(1).div(e).acosh()},"Array | Matrix":function(e){return oe(e,a)}});return a}),pu=["typed","config","Complex"],mu=Object(s.a)("asin",pu,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("asin",{number:function(e){return-1<=e&&e<=1||r.predictable?Math.asin(e):new n(e,0).asin()},Complex:function(e){return e.asin()},BigNumber:function(e){return e.asin()},"Array | Matrix":function(e){return oe(e,i,!0)}});return i}),hu=["typed"],du=Object(s.a)("asinh",hu,function(e){var t=(0,e.typed)("asinh",{number:Hs,Complex:function(e){return e.asinh()},BigNumber:function(e){return e.asinh()},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),yu=["typed"],gu=Object(s.a)("atan",yu,function(e){var t=(0,e.typed)("atan",{number:function(e){return Math.atan(e)},Complex:function(e){return e.atan()},BigNumber:function(e){return e.atan()},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),vu=["typed","matrix","equalScalar","BigNumber","DenseMatrix"],bu=Object(s.a)("atan2",vu,function(e){var t=e.typed,r=e.matrix,n=e.equalScalar,i=e.BigNumber,a=e.DenseMatrix,o=nr({typed:t,equalScalar:n}),s=dr({typed:t}),u=Hr({typed:t,equalScalar:n}),c=sr({typed:t,equalScalar:n}),f=br({typed:t,DenseMatrix:a}),l=Xt({typed:t}),p=Kt({typed:t}),m=t("atan2",{"number, number":Math.atan2,"BigNumber, BigNumber":function(e,t){return i.atan2(e,t)},"SparseMatrix, SparseMatrix":function(e,t){return u(e,t,m,!1)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,m,!0)},"DenseMatrix, SparseMatrix":function(e,t){return s(e,t,m,!1)},"DenseMatrix, DenseMatrix":function(e,t){return l(e,t,m)},"Array, Array":function(e,t){return m(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return m(r(e),t)},"Matrix, Array":function(e,t){return m(e,r(t))},"SparseMatrix, number | BigNumber":function(e,t){return c(e,t,m,!1)},"DenseMatrix, number | BigNumber":function(e,t){return p(e,t,m,!1)},"number | BigNumber, SparseMatrix":function(e,t){return f(t,e,m,!0)},"number | BigNumber, DenseMatrix":function(e,t){return p(t,e,m,!0)},"Array, number | BigNumber":function(e,t){return p(r(e),t,m,!1).valueOf()},"number | BigNumber, Array":function(e,t){return p(r(t),e,m,!0).valueOf()}});return m}),xu=["typed","config","Complex"],wu=Object(s.a)("atanh",xu,function(e){var t=e.typed,r=e.config,n=e.Complex,i=t("atanh",{number:function(e){return e<=1&&-1<=e||r.predictable?$s(e):new n(e,0).atanh()},Complex:function(e){return e.atanh()},BigNumber:function(e){return e.atanh()},"Array | Matrix":function(e){return oe(e,i,!0)}});return i}),Nu=["typed"],Ou=Object(s.a)("cos",Nu,function(e){var t=(0,e.typed)("cos",{number:Math.cos,Complex:function(e){return e.cos()},BigNumber:function(e){return e.cos()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function cos is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),Mu=["typed"],Eu=Object(s.a)("cosh",Mu,function(e){var t=(0,e.typed)("cosh",{number:j.e,Complex:function(e){return e.cosh()},BigNumber:function(e){return e.cosh()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function cosh is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t)}});return t}),ju=["typed","BigNumber"],Su=Object(s.a)("cot",ju,function(e){var t=e.typed,r=e.BigNumber,n=t("cot",{number:Gs,Complex:function(e){return e.cot()},BigNumber:function(e){return new r(1).div(e.tan())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function cot is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Au=["typed","BigNumber"],Cu=Object(s.a)("coth",Au,function(e){var t=e.typed,r=e.BigNumber,n=t("coth",{number:Zs,Complex:function(e){return e.coth()},BigNumber:function(e){return new r(1).div(e.tanh())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function coth is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Tu=["typed","BigNumber"],_u=Object(s.a)("csc",Tu,function(e){var t=e.typed,r=e.BigNumber,n=t("csc",{number:Vs,Complex:function(e){return e.csc()},BigNumber:function(e){return new r(1).div(e.sin())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function csc is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Iu=["typed","BigNumber"],qu=Object(s.a)("csch",Iu,function(e){var t=e.typed,r=e.BigNumber,n=t("csch",{number:Js,Complex:function(e){return e.csch()},BigNumber:function(e){return new r(1).div(e.sinh())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function csch is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Bu=["typed","BigNumber"],ku=Object(s.a)("sec",Bu,function(e){var t=e.typed,r=e.BigNumber,n=t("sec",{number:Ws,Complex:function(e){return e.sec()},BigNumber:function(e){return new r(1).div(e.cos())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function sec is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),zu=["typed","BigNumber"],Du=Object(s.a)("sech",zu,function(e){var t=e.typed,r=e.BigNumber,n=t("sech",{number:Ys,Complex:function(e){return e.sech()},BigNumber:function(e){return new r(1).div(e.cosh())},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function sech is no angle");return n(e.value)},"Array | Matrix":function(e){return oe(e,n)}});return n}),Ru=["typed"],Pu=Object(s.a)("sin",Ru,function(e){var t=(0,e.typed)("sin",{number:Math.sin,Complex:function(e){return e.sin()},BigNumber:function(e){return e.sin()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function sin is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),Fu=["typed"],Uu=Object(s.a)("sinh",Fu,function(e){var t=(0,e.typed)("sinh",{number:Xs,Complex:function(e){return e.sinh()},BigNumber:function(e){return e.sinh()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function sinh is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),Lu=["typed"],Hu=Object(s.a)("tan",Lu,function(e){var t=(0,e.typed)("tan",{number:Math.tan,Complex:function(e){return e.tan()},BigNumber:function(e){return e.tan()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function tan is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),$u=["typed"],Gu=Object(s.a)("tanh",$u,function(e){var t=(0,e.typed)("tanh",{number:j.p,Complex:function(e){return e.tanh()},BigNumber:function(e){return e.tanh()},Unit:function(e){if(!e.hasBase(e.constructor.BASE_UNITS.ANGLE))throw new TypeError("Unit in function tanh is no angle");return t(e.value)},"Array | Matrix":function(e){return oe(e,t,!0)}});return t}),Zu="setCartesian",Vu=["typed","size","subset","compareNatural","Index","DenseMatrix"],Ju=Object(s.a)(Zu,Vu,function(e){var t=e.typed,s=e.size,u=e.subset,c=e.compareNatural,f=e.Index,l=e.DenseMatrix;return t(Zu,{"Array | Matrix, Array | Matrix":function(e,t){var r=[];if(0!==u(s(e),new f(0))&&0!==u(s(t),new f(0))){var n=Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(c),i=Object(I.e)(Array.isArray(t)?t:t.toArray()).sort(c);r=[];for(var a=0;a<n.length;a++)for(var o=0;o<i.length;o++)r.push([n[a],i[o]])}return Array.isArray(e)&&Array.isArray(t)?r:new l(r)}})}),Wu="setDifference",Yu=["typed","size","subset","compareNatural","Index","DenseMatrix"],Xu=Object(s.a)(Wu,Yu,function(e){var t=e.typed,u=e.size,c=e.subset,f=e.compareNatural,l=e.Index,p=e.DenseMatrix;return t(Wu,{"Array | Matrix, Array | Matrix":function(e,t){var r;if(0===c(u(e),new l(0)))r=[];else{if(0===c(u(t),new l(0)))return Object(I.e)(e.toArray());var n,i=Object(I.i)(Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(f)),a=Object(I.i)(Object(I.e)(Array.isArray(t)?t:t.toArray()).sort(f));r=[];for(var o=0;o<i.length;o++){n=!1;for(var s=0;s<a.length;s++)if(0===f(i[o].value,a[s].value)&&i[o].identifier===a[s].identifier){n=!0;break}n||r.push(i[o])}}return Array.isArray(e)&&Array.isArray(t)?Object(I.g)(r):new p(Object(I.g)(r))}})}),Qu="setDistinct",Ku=["typed","size","subset","compareNatural","Index","DenseMatrix"],ec=Object(s.a)(Qu,Ku,function(e){var t=e.typed,i=e.size,a=e.subset,o=e.compareNatural,s=e.Index,u=e.DenseMatrix;return t(Qu,{"Array | Matrix":function(e){var t;if(0===a(i(e),new s(0)))t=[];else{var r=Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(o);(t=[]).push(r[0]);for(var n=1;n<r.length;n++)0!==o(r[n],r[n-1])&&t.push(r[n])}return Array.isArray(e)?t:new u(t)}})}),tc="setIntersect",rc=["typed","size","subset","compareNatural","Index","DenseMatrix"],nc=Object(s.a)(tc,rc,function(e){var t=e.typed,s=e.size,u=e.subset,c=e.compareNatural,f=e.Index,l=e.DenseMatrix;return t(tc,{"Array | Matrix, Array | Matrix":function(e,t){var r;if(0===u(s(e),new f(0))||0===u(s(t),new f(0)))r=[];else{var n=Object(I.i)(Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(c)),i=Object(I.i)(Object(I.e)(Array.isArray(t)?t:t.toArray()).sort(c));r=[];for(var a=0;a<n.length;a++)for(var o=0;o<i.length;o++)if(0===c(n[a].value,i[o].value)&&n[a].identifier===i[o].identifier){r.push(n[a]);break}}return Array.isArray(e)&&Array.isArray(t)?Object(I.g)(r):new l(Object(I.g)(r))}})}),ic="setIsSubset",ac=["typed","size","subset","compareNatural","Index"],oc=Object(s.a)(ic,ac,function(e){var t=e.typed,s=e.size,u=e.subset,c=e.compareNatural,f=e.Index;return t(ic,{"Array | Matrix, Array | Matrix":function(e,t){if(0===u(s(e),new f(0)))return!0;if(0===u(s(t),new f(0)))return!1;for(var r,n=Object(I.i)(Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(c)),i=Object(I.i)(Object(I.e)(Array.isArray(t)?t:t.toArray()).sort(c)),a=0;a<n.length;a++){r=!1;for(var o=0;o<i.length;o++)if(0===c(n[a].value,i[o].value)&&n[a].identifier===i[o].identifier){r=!0;break}if(!1===r)return!1}return!0}})}),sc="setMultiplicity",uc=["typed","size","subset","compareNatural","Index"],cc=Object(s.a)(sc,uc,function(e){var t=e.typed,a=e.size,o=e.subset,s=e.compareNatural,u=e.Index;return t(sc,{"number | BigNumber | Fraction | Complex, Array | Matrix":function(e,t){if(0===o(a(t),new u(0)))return 0;for(var r=Object(I.e)(Array.isArray(t)?t:t.toArray()),n=0,i=0;i<r.length;i++)0===s(r[i],e)&&n++;return n}})}),fc="setPowerset",lc=["typed","size","subset","compareNatural","Index"],pc=Object(s.a)(fc,lc,function(e){var t=e.typed,i=e.size,a=e.subset,o=e.compareNatural,s=e.Index;return t(fc,{"Array | Matrix":function(e){if(0===a(i(e),new s(0)))return[];for(var t=Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(o),r=[],n=0;n.toString(2).length<=t.length;)r.push(u(t,n.toString(2).split("").reverse())),n++;return function(e){for(var t=[],r=e.length-1;0<r;r--)for(var n=0;n<r;n++)e[n].length>e[n+1].length&&(t=e[n],e[n]=e[n+1],e[n+1]=t);return e}(r)}});function u(e,t){for(var r=[],n=0;n<t.length;n++)"1"===t[n]&&r.push(e[n]);return r}}),mc="setSize",hc=["typed","compareNatural"],dc=Object(s.a)(mc,hc,function(e){var t=e.typed,a=e.compareNatural;return t(mc,{"Array | Matrix":function(e){return Array.isArray(e)?Object(I.e)(e).length:Object(I.e)(e.toArray()).length},"Array | Matrix, boolean":function(e,t){if(!1===t||0===e.length)return Array.isArray(e)?Object(I.e)(e).length:Object(I.e)(e.toArray()).length;for(var r=Object(I.e)(Array.isArray(e)?e:e.toArray()).sort(a),n=1,i=1;i<r.length;i++)0!==a(r[i],r[i-1])&&n++;return n}})}),yc="setSymDifference",gc=["typed","size","concat","subset","setDifference","Index"],vc=Object(s.a)(yc,gc,function(e){var t=e.typed,i=e.size,a=e.concat,o=e.subset,s=e.setDifference,u=e.Index;return t(yc,{"Array | Matrix, Array | Matrix":function(e,t){if(0===o(i(e),new u(0)))return Object(I.e)(t);if(0===o(i(t),new u(0)))return Object(I.e)(e);var r=Object(I.e)(e),n=Object(I.e)(t);return a(s(r,n),s(n,r))}})}),bc="setUnion",xc=["typed","size","concat","subset","setIntersect","setSymDifference","Index"],wc=Object(s.a)(bc,xc,function(e){var t=e.typed,i=e.size,a=e.concat,o=e.subset,s=e.setIntersect,u=e.setSymDifference,c=e.Index;return t(bc,{"Array | Matrix, Array | Matrix":function(e,t){if(0===o(i(e),new c(0)))return Object(I.e)(t);if(0===o(i(t),new c(0)))return Object(I.e)(e);var r=Object(I.e)(e),n=Object(I.e)(t);return a(u(r,n),s(r,n))}})}),Nc=["typed","matrix","addScalar","equalScalar","DenseMatrix","SparseMatrix"],Oc=Object(s.a)("add",Nc,function(e){var t=e.typed,r=e.matrix,n=e.addScalar,i=e.equalScalar,a=e.DenseMatrix,o=(e.SparseMatrix,Gt({typed:t})),s=Vt({typed:t,equalScalar:i}),u=Wt({typed:t,DenseMatrix:a}),c=Xt({typed:t}),f=Kt({typed:t}),l=t("add",Object(ae.e)({"DenseMatrix, DenseMatrix":function(e,t){return c(e,t,n)},"DenseMatrix, SparseMatrix":function(e,t){return o(e,t,n,!1)},"SparseMatrix, DenseMatrix":function(e,t){return o(t,e,n,!0)},"SparseMatrix, SparseMatrix":function(e,t){return s(e,t,n)},"Array, Array":function(e,t){return l(r(e),r(t)).valueOf()},"Array, Matrix":function(e,t){return l(r(e),t)},"Matrix, Array":function(e,t){return l(e,r(t))},"DenseMatrix, any":function(e,t){return f(e,t,n,!1)},"SparseMatrix, any":function(e,t){return u(e,t,n,!1)},"any, DenseMatrix":function(e,t){return f(t,e,n,!0)},"any, SparseMatrix":function(e,t){return u(t,e,n,!0)},"Array, any":function(e,t){return f(r(e),t,n,!1).valueOf()},"any, Array":function(e,t){return f(r(t),e,n,!0).valueOf()},"any, any":n,"any, any, ...any":function(e,t,r){for(var n=l(e,t),i=0;i<r.length;i++)n=l(n,r[i]);return n}},n.signatures));return l}),Mc=["typed","abs","addScalar","divideScalar","multiplyScalar","sqrt","smaller","isPositive"],Ec=Object(s.a)("hypot",Mc,function(e){var t=e.typed,a=e.abs,o=e.addScalar,s=e.divideScalar,u=e.multiplyScalar,c=e.sqrt,f=e.smaller,l=e.isPositive,r=t("hypot",{"... number | BigNumber":function(e){for(var t=0,r=0,n=0;n<e.length;n++){var i=a(e[n]);f(r,i)?(t=u(t,u(s(r,i),s(r,i))),t=o(t,1),r=i):t=o(t,l(i)?u(s(i,r),s(i,r)):i)}return u(r,c(t))},Array:function(e){return r.apply(r,Object(I.e)(e))},Matrix:function(e){return r.apply(r,Object(I.e)(e.toArray()))}});return r}),jc=["typed","abs","add","pow","conj","sqrt","multiply","equalScalar","larger","smaller","matrix"],Sc=Object(s.a)("norm",jc,function(e){var t=e.typed,l=e.abs,p=e.add,m=e.pow,h=e.conj,d=e.sqrt,y=e.multiply,g=e.equalScalar,v=e.larger,b=e.smaller,r=e.matrix,n=t("norm",{number:Math.abs,Complex:function(e){return e.abs()},BigNumber:function(e){return e.abs()},boolean:function(e){return Math.abs(e)},Array:function(e){return x(r(e),2)},Matrix:function(e){return x(e,2)},"number | Complex | BigNumber | boolean, number | BigNumber | string":function(e){return n(e)},"Array, number | BigNumber | string":function(e,t){return x(r(e),t)},"Matrix, number | BigNumber | string":function(e,t){return x(e,t)}});function x(e,t){var r=e.size();if(1===r.length){if(t===Number.POSITIVE_INFINITY||"inf"===t){var n=0;return e.forEach(function(e){var t=l(e);v(t,n)&&(n=t)},!0),n}var i;if(t===Number.NEGATIVE_INFINITY||"-inf"===t)return e.forEach(function(e){var t=l(e);i&&!b(t,i)||(i=t)},!0),i||0;if("fro"===t)return x(e,2);if("number"!=typeof t||isNaN(t))throw new Error("Unsupported parameter value");if(g(t,0))return Number.POSITIVE_INFINITY;var a=0;return e.forEach(function(e){a=p(m(l(e),t),a)},!0),m(a,1/t)}if(2===r.length){if(1===t){var o=[],s=0;return e.forEach(function(e,t){var r=t[1],n=p(o[r]||0,l(e));v(n,s)&&(s=n),o[r]=n},!0),s}if(t===Number.POSITIVE_INFINITY||"inf"===t){var u=[],c=0;return e.forEach(function(e,t){var r=t[0],n=p(u[r]||0,l(e));v(n,c)&&(c=n),u[r]=n},!0),c}if("fro"===t){var f=0;return e.forEach(function(e,t){f=p(f,y(e,h(e)))}),l(d(f))}if(2===t)throw new Error("Unsupported parameter value, missing implementation of matrix singular value decomposition");throw new Error("Unsupported parameter value")}}return n}),Ac=["typed","add","multiply"],Cc=Object(s.a)("dot",Ac,function(e){var t=e.typed,s=e.add,u=e.multiply;return t("dot",{"Matrix, Matrix":function(e,t){return r(e.toArray(),t.toArray())},"Matrix, Array":function(e,t){return r(e.toArray(),t)},"Array, Matrix":function(e,t){return r(e,t.toArray())},"Array, Array":r});function r(e,t){var r=Object(I.a)(e),n=Object(I.a)(t),i=r[0];if(1!==r.length||1!==n.length)throw new RangeError("Vector expected");if(r[0]!==n[0])throw new RangeError("Vectors must have equal length ("+r[0]+" != "+n[0]+")");if(0===i)throw new RangeError("Cannot calculate the dot product of empty vectors");for(var a=0,o=0;o<i;o++)a=s(a,u(e[o],t[o]));return a}}),Tc=["typed","matrix","add"],_c=Object(s.a)("trace",Tc,function(e){var t=e.typed,r=e.matrix,m=e.add;return t("trace",{Array:function(e){return n(r(e))},SparseMatrix:function(e){var t=e._values,r=e._index,n=e._ptr,i=e._size,a=i[0],o=i[1];if(a!==o)throw new RangeError("Matrix must be square (size: "+Object(J.d)(i)+")");var s=0;if(0<t.length)for(var u=0;u<o;u++)for(var c=n[u],f=n[u+1],l=c;l<f;l++){var p=r[l];if(p===u){s=m(s,t[l]);break}if(u<p)break}return s},DenseMatrix:n,any:ae.a});function n(e){var t=e._size,r=e._data;switch(t.length){case 1:if(1===t[0])return Object(ae.a)(r[0]);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");case 2:var n=t[0];if(n!==t[1])throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");for(var i=0,a=0;a<n;a++)i=m(i,r[a][a]);return i;default:throw new RangeError("Matrix must be two dimensional (size: "+Object(J.d)(t)+")")}}}),Ic=["typed","Index"],qc=Object(s.a)("index",Ic,function(e){var t=e.typed,n=e.Index;return t("index",{"...number | string | BigNumber | Range | Array | Matrix":function(e){var t=e.map(function(e){return Object(ie.e)(e)?e.toNumber():Array.isArray(e)||Object(ie.v)(e)?e.map(function(e){return Object(ie.e)(e)?e.toNumber():e}):e}),r=new n;return n.apply(r,t),r}})}),Bc={end:!0};function kc(e){return(kc="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var zc=["mathWithTransform"],Dc=Object(s.a)("Node",zc,function(e){var a=e.mathWithTransform;function t(){if(!(this instanceof t))throw new SyntaxError("Constructor must be called with the new operator")}return t.prototype.evaluate=function(e){return this.compile().evaluate(e)},t.prototype.eval=function(e){return Object(ve.a)("Method Node.eval is renamed to Node.evaluate. Please use the new method name."),this.evaluate(e)},t.prototype.type="Node",t.prototype.isNode=!0,t.prototype.comment="",t.prototype.compile=function(){var r=this._compile(a,{}),n={},i=null;function t(e){var t=e||{};return function(e){for(var t in e)if(Object(ae.f)(e,t)&&t in Bc)throw new Error('Scope contains an illegal symbol, "'+t+'" is a reserved keyword')}(t),r(t,n,i)}return{evaluate:t,eval:function(e){return Object(ve.a)("Method eval is renamed to evaluate. Please use the new method."),t(e)}}},t.prototype._compile=function(e,t){throw new Error("Method _compile should be implemented by type "+this.type)},t.prototype.forEach=function(e){throw new Error("Cannot run forEach on a Node interface")},t.prototype.map=function(e){throw new Error("Cannot run map on a Node interface")},t.prototype._ifNode=function(e){if(!Object(ie.w)(e))throw new TypeError("Callback function must return a Node");return e},t.prototype.traverse=function(e){e(this,null,null),function n(e,i){e.forEach(function(e,t,r){i(e,t,r),n(e,i)})}(this,e)},t.prototype.transform=function(a){return function e(t,r,n){var i=a(t,r,n);return i!==t?i:t.map(e)}(this,null,null)},t.prototype.filter=function(n){var i=[];return this.traverse(function(e,t,r){n(e,t,r)&&i.push(e)}),i},t.prototype.find=function(){throw new Error("Function Node.find is deprecated. Use Node.filter instead.")},t.prototype.match=function(){throw new Error("Function Node.match is deprecated. See functions Node.filter, Node.transform, Node.traverse.")},t.prototype.clone=function(){throw new Error("Cannot clone a Node interface")},t.prototype.cloneDeep=function(){return this.map(function(e){return e.cloneDeep()})},t.prototype.equals=function(e){return!!e&&Object(ae.d)(this,e)},t.prototype.toString=function(e){var t;if(e&&"object"===kc(e))switch(kc(e.handler)){case"object":case"undefined":break;case"function":t=e.handler(this,e);break;default:throw new TypeError("Object or function expected as callback")}return void 0!==t?t:this._toString(e)},t.prototype.toJSON=function(){throw new Error("Cannot serialize object: toJSON not implemented by "+this.type)},t.prototype.toHTML=function(e){var t;if(e&&"object"===kc(e))switch(kc(e.handler)){case"object":case"undefined":break;case"function":t=e.handler(this,e);break;default:throw new TypeError("Object or function expected as callback")}return void 0!==t?t:this.toHTML(e)},t.prototype._toString=function(){throw new Error("_toString not implemented for "+this.type)},t.prototype.toTex=function(e){var t;if(e&&"object"===kc(e))switch(kc(e.handler)){case"object":case"undefined":break;case"function":t=e.handler(this,e);break;default:throw new TypeError("Object or function expected as callback")}return void 0!==t?t:this._toTex(e)},t.prototype._toTex=function(e){throw new Error("_toTex not implemented for "+this.type)},t.prototype.getIdentifier=function(){return this.type},t.prototype.getContent=function(){return this},t},{isClass:!0,isNode:!0});function Rc(e){return e&&e.isIndexError?new R.a(e.index+1,e.min+1,void 0!==e.max?e.max+1:void 0):e}function Pc(e){return(Pc="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Fc(e){var r=e.subset;return function(e,t){try{if(Array.isArray(e))return r(e,t);if(e&&"function"==typeof e.subset)return e.subset(t);if("string"==typeof e)return r(e,t);if("object"!==Pc(e))throw new TypeError("Cannot apply index: unsupported type of object");if(!t.isObjectProperty())throw new TypeError("Cannot apply a numeric index as object property");return Fi(e,t.getObjectProperty())}catch(e){throw Rc(e)}}}var Uc=["subset","Node"],Lc=Object(s.a)("AccessorNode",Uc,function(e){var t=e.subset,r=e.Node,s=Fc({subset:t});function n(e,t){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError('Node expected for parameter "object"');if(!Object(ie.u)(t))throw new TypeError('IndexNode expected for parameter "index"');this.object=e||null,this.index=t,Object.defineProperty(this,"name",{get:function(){return this.index?this.index.isObjectProperty()?this.index.getObjectProperty():"":this.object.name||""}.bind(this),set:function(){throw new Error("Cannot assign a new name, name is read-only")}})}function i(e){return!(Object(ie.a)(e)||Object(ie.c)(e)||Object(ie.l)(e)||Object(ie.r)(e)||Object(ie.A)(e)||Object(ie.C)(e)||Object(ie.J)(e))}return(n.prototype=new r).type="AccessorNode",n.prototype.isAccessorNode=!0,n.prototype._compile=function(e,t){var a=this.object._compile(e,t),o=this.index._compile(e,t);if(this.index.isObjectProperty()){var n=this.index.getObjectProperty();return function(e,t,r){return Fi(a(e,t,r),n)}}return function(e,t,r){var n=a(e,t,r),i=o(e,t,n);return s(n,i)}},n.prototype.forEach=function(e){e(this.object,"object",this),e(this.index,"index",this)},n.prototype.map=function(e){return new n(this._ifNode(e(this.object,"object",this)),this._ifNode(e(this.index,"index",this)))},n.prototype.clone=function(){return new n(this.object,this.index)},n.prototype._toString=function(e){var t=this.object.toString(e);return i(this.object)&&(t="("+t+")"),t+this.index.toString(e)},n.prototype.toHTML=function(e){var t=this.object.toHTML(e);return i(this.object)&&(t='<span class="math-parenthesis math-round-parenthesis">(</span>'+t+'<span class="math-parenthesis math-round-parenthesis">)</span>'),t+this.index.toHTML(e)},n.prototype._toTex=function(e){var t=this.object.toTex(e);return i(this.object)&&(t="\\left(' + object + '\\right)"),t+this.index.toTex(e)},n.prototype.toJSON=function(){return{mathjs:"AccessorNode",object:this.object,index:this.index}},n.fromJSON=function(e){return new n(e.object,e.index)},n},{isClass:!0,isNode:!0}),Hc=["Node"],$c=Object(s.a)("ArrayNode",Hc,function(e){var t=e.Node;function n(e){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(this.items=e||[],!Array.isArray(this.items)||!this.items.every(ie.w))throw new TypeError("Array containing Nodes expected");function t(){throw new Error("Property `ArrayNode.nodes` is deprecated, use `ArrayNode.items` instead")}Object.defineProperty(this,"nodes",{get:t,set:t})}return(n.prototype=new t).type="ArrayNode",n.prototype.isArrayNode=!0,n.prototype._compile=function(t,r){var e=Object(I.m)(this.items,function(e){return e._compile(t,r)});if("Array"===t.config.matrix)return function(t,r,n){return Object(I.m)(e,function(e){return e(t,r,n)})};var i=t.matrix;return function(t,r,n){return i(Object(I.m)(e,function(e){return e(t,r,n)}))}},n.prototype.forEach=function(e){for(var t=0;t<this.items.length;t++){e(this.items[t],"items["+t+"]",this)}},n.prototype.map=function(e){for(var t=[],r=0;r<this.items.length;r++)t[r]=this._ifNode(e(this.items[r],"items["+r+"]",this));return new n(t)},n.prototype.clone=function(){return new n(this.items.slice(0))},n.prototype._toString=function(t){return"["+this.items.map(function(e){return e.toString(t)}).join(", ")+"]"},n.prototype.toJSON=function(){return{mathjs:"ArrayNode",items:this.items}},n.fromJSON=function(e){return new n(e.items)},n.prototype.toHTML=function(t){return'<span class="math-parenthesis math-square-parenthesis">[</span>'+this.items.map(function(e){return e.toHTML(t)}).join('<span class="math-separator">,</span>')+'<span class="math-parenthesis math-square-parenthesis">]</span>'},n.prototype._toTex=function(t){var r="\\begin{bmatrix}";return this.items.forEach(function(e){e.items?r+=e.items.map(function(e){return e.toTex(t)}).join("&"):r+=e.toTex(t),r+="\\\\"}),r+="\\end{bmatrix}"},n},{isClass:!0,isNode:!0});function Gc(e){return(Gc="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var Zc=[{AssignmentNode:{},FunctionAssignmentNode:{}},{ConditionalNode:{latexLeftParens:!1,latexRightParens:!1,latexParens:!1}},{"OperatorNode:or":{associativity:"left",associativeWith:[]}},{"OperatorNode:xor":{associativity:"left",associativeWith:[]}},{"OperatorNode:and":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitOr":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitXor":{associativity:"left",associativeWith:[]}},{"OperatorNode:bitAnd":{associativity:"left",associativeWith:[]}},{"OperatorNode:equal":{associativity:"left",associativeWith:[]},"OperatorNode:unequal":{associativity:"left",associativeWith:[]},"OperatorNode:smaller":{associativity:"left",associativeWith:[]},"OperatorNode:larger":{associativity:"left",associativeWith:[]},"OperatorNode:smallerEq":{associativity:"left",associativeWith:[]},"OperatorNode:largerEq":{associativity:"left",associativeWith:[]},RelationalNode:{associativity:"left",associativeWith:[]}},{"OperatorNode:leftShift":{associativity:"left",associativeWith:[]},"OperatorNode:rightArithShift":{associativity:"left",associativeWith:[]},"OperatorNode:rightLogShift":{associativity:"left",associativeWith:[]}},{"OperatorNode:to":{associativity:"left",associativeWith:[]}},{RangeNode:{}},{"OperatorNode:add":{associativity:"left",associativeWith:["OperatorNode:add","OperatorNode:subtract"]},"OperatorNode:subtract":{associativity:"left",associativeWith:[]}},{"OperatorNode:multiply":{associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","Operator:dotMultiply","Operator:dotDivide"]},"OperatorNode:divide":{associativity:"left",associativeWith:[],latexLeftParens:!1,latexRightParens:!1,latexParens:!1},"OperatorNode:dotMultiply":{associativity:"left",associativeWith:["OperatorNode:multiply","OperatorNode:divide","OperatorNode:dotMultiply","OperatorNode:doDivide"]},"OperatorNode:dotDivide":{associativity:"left",associativeWith:[]},"OperatorNode:mod":{associativity:"left",associativeWith:[]}},{"OperatorNode:unaryPlus":{associativity:"right"},"OperatorNode:unaryMinus":{associativity:"right"},"OperatorNode:bitNot":{associativity:"right"},"OperatorNode:not":{associativity:"right"}},{"OperatorNode:pow":{associativity:"right",associativeWith:[],latexRightParens:!1},"OperatorNode:dotPow":{associativity:"right",associativeWith:[]}},{"OperatorNode:factorial":{associativity:"left"}},{"OperatorNode:transpose":{associativity:"left"}}];function Vc(e,t){var r=e;"keep"!==t&&(r=e.getContent());for(var n=r.getIdentifier(),i=0;i<Zc.length;i++)if(n in Zc[i])return i;return null}function Jc(e,t){var r=e;"keep"!==t&&(r=e.getContent());var n=r.getIdentifier(),i=Vc(r,t);if(null===i)return null;var a=Zc[i][n];if(Object(ae.f)(a,"associativity")){if("left"===a.associativity)return"left";if("right"===a.associativity)return"right";throw Error("'"+n+"' has the invalid associativity '"+a.associativity+"'.")}return null}function Wc(e,t,r){var n="keep"!==r?e.getContent():e,i="keep"!==r?e.getContent():t,a=n.getIdentifier(),o=i.getIdentifier(),s=Vc(n,r);if(null===s)return null;var u=Zc[s][a];if(Object(ae.f)(u,"associativeWith")&&u.associativeWith instanceof Array){for(var c=0;c<u.associativeWith.length;c++)if(u.associativeWith[c]===o)return!0;return!1}return null}var Yc=["subset","?matrix","Node"],Xc=Object(s.a)("AssignmentNode",Yc,function(e){var t=e.subset,r=e.matrix,n=e.Node,m=Fc({subset:t}),h=function(e){var n=e.subset,i=e.matrix;return function(e,t,r){try{if(Array.isArray(e))return i(e).subset(t,r).valueOf();if(e&&"function"==typeof e.subset)return e.subset(t,r);if("string"==typeof e)return n(e,t,r);if("object"!==Gc(e))throw new TypeError("Cannot apply index: unsupported type of object");if(!t.isObjectProperty())throw TypeError("Cannot apply a numeric index as object property");return Ui(e,t.getObjectProperty(),r),e}catch(e){throw Rc(e)}}}({subset:t,matrix:r});function i(e,t,r){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if(this.object=e,this.index=r?t:null,this.value=r||t,!Object(ie.J)(e)&&!Object(ie.a)(e))throw new TypeError('SymbolNode or AccessorNode expected as "object"');if(Object(ie.J)(e)&&"end"===e.name)throw new Error('Cannot assign to symbol "end"');if(this.index&&!Object(ie.u)(this.index))throw new TypeError('IndexNode expected as "index"');if(!Object(ie.w)(this.value))throw new TypeError('Node expected as "value"');Object.defineProperty(this,"name",{get:function(){return this.index?this.index.isObjectProperty()?this.index.getObjectProperty():"":this.object.name||""}.bind(this),set:function(){throw new Error("Cannot assign a new name, name is read-only")}})}function a(e,t){var r=Vc(e,t=t||"keep"),n=Vc(e.value,t);return"all"===t||null!==n&&n<=r}return(i.prototype=new n).type="AssignmentNode",i.prototype.isAssignmentNode=!0,i.prototype._compile=function(e,t){var o=this.object._compile(e,t),u=this.index?this.index._compile(e,t):null,c=this.value._compile(e,t),s=this.object.name;if(this.index){if(this.index.isObjectProperty()){var a=this.index.getObjectProperty();return function(e,t,r){var n=o(e,t,r),i=c(e,t,r);return Ui(n,a,i)}}if(Object(ie.J)(this.object))return function(e,t,r){var n=o(e,t,r),i=c(e,t,r),a=u(e,t,n);return Ui(e,s,h(n,a,i)),i};var f=this.object.object._compile(e,t);if(this.object.index.isObjectProperty()){var l=this.object.index.getObjectProperty();return function(e,t,r){var n=f(e,t,r),i=Fi(n,l),a=u(e,t,i),o=c(e,t,r);return Ui(n,l,h(i,a,o)),o}}var p=this.object.index._compile(e,t);return function(e,t,r){var n=f(e,t,r),i=p(e,t,n),a=m(n,i),o=u(e,t,a),s=c(e,t,r);return h(n,i,h(a,o,s)),s}}if(!Object(ie.J)(this.object))throw new TypeError("SymbolNode expected as object");return function(e,t,r){return Ui(e,s,c(e,t,r))}},i.prototype.forEach=function(e){e(this.object,"object",this),this.index&&e(this.index,"index",this),e(this.value,"value",this)},i.prototype.map=function(e){return new i(this._ifNode(e(this.object,"object",this)),this.index?this._ifNode(e(this.index,"index",this)):null,this._ifNode(e(this.value,"value",this)))},i.prototype.clone=function(){return new i(this.object,this.index,this.value)},i.prototype._toString=function(e){var t=this.object.toString(e),r=this.index?this.index.toString(e):"",n=this.value.toString(e);return a(this,e&&e.parenthesis)&&(n="("+n+")"),t+r+" = "+n},i.prototype.toJSON=function(){return{mathjs:"AssignmentNode",object:this.object,index:this.index,value:this.value}},i.fromJSON=function(e){return new i(e.object,e.index,e.value)},i.prototype.toHTML=function(e){var t=this.object.toHTML(e),r=this.index?this.index.toHTML(e):"",n=this.value.toHTML(e);return a(this,e&&e.parenthesis)&&(n='<span class="math-paranthesis math-round-parenthesis">(</span>'+n+'<span class="math-paranthesis math-round-parenthesis">)</span>'),t+r+'<span class="math-operator math-assignment-operator math-variable-assignment-operator math-binary-operator">=</span>'+n},i.prototype._toTex=function(e){var t=this.object.toTex(e),r=this.index?this.index.toTex(e):"",n=this.value.toTex(e);return a(this,e&&e.parenthesis)&&(n="\\left(".concat(n,"\\right)")),t+r+":="+n},i},{isClass:!0,isNode:!0}),Qc=["ResultSet","Node"],Kc=Object(s.a)("BlockNode",Qc,function(e){var o=e.ResultSet,t=e.Node;function a(e){if(!(this instanceof a))throw new SyntaxError("Constructor must be called with the new operator");if(!Array.isArray(e))throw new Error("Array expected");this.blocks=e.map(function(e){var t=e&&e.node,r=!e||void 0===e.visible||e.visible;if(!Object(ie.w)(t))throw new TypeError('Property "node" must be a Node');if("boolean"!=typeof r)throw new TypeError('Property "visible" must be a boolean');return{node:t,visible:r}})}return(a.prototype=new t).type="BlockNode",a.prototype.isBlockNode=!0,a.prototype._compile=function(t,r){var e=Object(I.m)(this.blocks,function(e){return{evaluate:e.node._compile(t,r),visible:e.visible}});return function(r,n,i){var a=[];return Object(I.f)(e,function(e){var t=e.evaluate(r,n,i);e.visible&&a.push(t)}),new o(a)}},a.prototype.forEach=function(e){for(var t=0;t<this.blocks.length;t++)e(this.blocks[t].node,"blocks["+t+"].node",this)},a.prototype.map=function(e){for(var t=[],r=0;r<this.blocks.length;r++){var n=this.blocks[r],i=this._ifNode(e(n.node,"blocks["+r+"].node",this));t[r]={node:i,visible:n.visible}}return new a(t)},a.prototype.clone=function(){return new a(this.blocks.map(function(e){return{node:e.node,visible:e.visible}}))},a.prototype._toString=function(t){return this.blocks.map(function(e){return e.node.toString(t)+(e.visible?"":";")}).join("\n")},a.prototype.toJSON=function(){return{mathjs:"BlockNode",blocks:this.blocks}},a.fromJSON=function(e){return new a(e.blocks)},a.prototype.toHTML=function(t){return this.blocks.map(function(e){return e.node.toHTML(t)+(e.visible?"":'<span class="math-separator">;</span>')}).join('<span class="math-separator"><br /></span>')},a.prototype._toTex=function(t){return this.blocks.map(function(e){return e.node.toTex(t)+(e.visible?"":";")}).join("\\;\\;\n")},a},{isClass:!0,isNode:!0}),ef=["Node"],tf=Object(s.a)("ConditionalNode",ef,function(e){var t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError("Parameter condition must be a Node");if(!Object(ie.w)(t))throw new TypeError("Parameter trueExpr must be a Node");if(!Object(ie.w)(r))throw new TypeError("Parameter falseExpr must be a Node");this.condition=e,this.trueExpr=t,this.falseExpr=r}return(n.prototype=new t).type="ConditionalNode",n.prototype.isConditionalNode=!0,n.prototype._compile=function(e,t){var n=this.condition._compile(e,t),i=this.trueExpr._compile(e,t),a=this.falseExpr._compile(e,t);return function(e,t,r){return function(e){if("number"==typeof e||"boolean"==typeof e||"string"==typeof e)return!!e;if(e){if(Object(ie.e)(e))return!e.isZero();if(Object(ie.j)(e))return!(!e.re&&!e.im);if(Object(ie.L)(e))return!!e.value}if(null!=e)throw new TypeError('Unsupported type of condition "'+Object(ie.M)(e)+'"');return!1}(n(e,t,r))?i(e,t,r):a(e,t,r)}},n.prototype.forEach=function(e){e(this.condition,"condition",this),e(this.trueExpr,"trueExpr",this),e(this.falseExpr,"falseExpr",this)},n.prototype.map=function(e){return new n(this._ifNode(e(this.condition,"condition",this)),this._ifNode(e(this.trueExpr,"trueExpr",this)),this._ifNode(e(this.falseExpr,"falseExpr",this)))},n.prototype.clone=function(){return new n(this.condition,this.trueExpr,this.falseExpr)},n.prototype._toString=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=Vc(this,t),n=this.condition.toString(e),i=Vc(this.condition,t);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n="("+n+")");var a=this.trueExpr.toString(e),o=Vc(this.trueExpr,t);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a="("+a+")");var s=this.falseExpr.toString(e),u=Vc(this.falseExpr,t);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==u&&u<=r)&&(s="("+s+")"),n+" ? "+a+" : "+s},n.prototype.toJSON=function(){return{mathjs:"ConditionalNode",condition:this.condition,trueExpr:this.trueExpr,falseExpr:this.falseExpr}},n.fromJSON=function(e){return new n(e.condition,e.trueExpr,e.falseExpr)},n.prototype.toHTML=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=Vc(this,t),n=this.condition.toHTML(e),i=Vc(this.condition,t);("all"===t||"OperatorNode"===this.condition.type||null!==i&&i<=r)&&(n='<span class="math-parenthesis math-round-parenthesis">(</span>'+n+'<span class="math-parenthesis math-round-parenthesis">)</span>');var a=this.trueExpr.toHTML(e),o=Vc(this.trueExpr,t);("all"===t||"OperatorNode"===this.trueExpr.type||null!==o&&o<=r)&&(a='<span class="math-parenthesis math-round-parenthesis">(</span>'+a+'<span class="math-parenthesis math-round-parenthesis">)</span>');var s=this.falseExpr.toHTML(e),u=Vc(this.falseExpr,t);return("all"===t||"OperatorNode"===this.falseExpr.type||null!==u&&u<=r)&&(s='<span class="math-parenthesis math-round-parenthesis">(</span>'+s+'<span class="math-parenthesis math-round-parenthesis">)</span>'),n+'<span class="math-operator math-conditional-operator">?</span>'+a+'<span class="math-operator math-conditional-operator">:</span>'+s},n.prototype._toTex=function(e){return"\\begin{cases} {"+this.trueExpr.toTex(e)+"}, &\\quad{\\text{if }\\;"+this.condition.toTex(e)+"}\\\\{"+this.falseExpr.toTex(e)+"}, &\\quad{\\text{otherwise}}\\end{cases}"},n},{isClass:!0,isNode:!0}),rf=r(17),nf=r.n(rf),af={Alpha:"A",alpha:"\\alpha",Beta:"B",beta:"\\beta",Gamma:"\\Gamma",gamma:"\\gamma",Delta:"\\Delta",delta:"\\delta",Epsilon:"E",epsilon:"\\epsilon",varepsilon:"\\varepsilon",Zeta:"Z",zeta:"\\zeta",Eta:"H",eta:"\\eta",Theta:"\\Theta",theta:"\\theta",vartheta:"\\vartheta",Iota:"I",iota:"\\iota",Kappa:"K",kappa:"\\kappa",varkappa:"\\varkappa",Lambda:"\\Lambda",lambda:"\\lambda",Mu:"M",mu:"\\mu",Nu:"N",nu:"\\nu",Xi:"\\Xi",xi:"\\xi",Omicron:"O",omicron:"o",Pi:"\\Pi",pi:"\\pi",varpi:"\\varpi",Rho:"P",rho:"\\rho",varrho:"\\varrho",Sigma:"\\Sigma",sigma:"\\sigma",varsigma:"\\varsigma",Tau:"T",tau:"\\tau",Upsilon:"\\Upsilon",upsilon:"\\upsilon",Phi:"\\Phi",phi:"\\phi",varphi:"\\varphi",Chi:"X",chi:"\\chi",Psi:"\\Psi",psi:"\\psi",Omega:"\\Omega",omega:"\\omega",true:"\\mathrm{True}",false:"\\mathrm{False}",i:"i",inf:"\\infty",Inf:"\\infty",infinity:"\\infty",Infinity:"\\infty",oo:"\\infty",lim:"\\lim",undefined:"\\mathbf{?}"},of={transpose:"^\\top",ctranspose:"^H",factorial:"!",pow:"^",dotPow:".^\\wedge",unaryPlus:"+",unaryMinus:"-",bitNot:"\\~",not:"\\neg",multiply:"\\cdot",divide:"\\frac",dotMultiply:".\\cdot",dotDivide:".:",mod:"\\mod",add:"+",subtract:"-",to:"\\rightarrow",leftShift:"<<",rightArithShift:">>",rightLogShift:">>>",equal:"=",unequal:"\\neq",smaller:"<",larger:">",smallerEq:"\\leq",largerEq:"\\geq",bitAnd:"\\&",bitXor:"\\underline{|}",bitOr:"|",and:"\\wedge",xor:"\\veebar",or:"\\vee"},sf={abs:{1:"\\left|${args[0]}\\right|"},add:{2:"\\left(${args[0]}".concat(of.add,"${args[1]}\\right)")},cbrt:{1:"\\sqrt[3]{${args[0]}}"},ceil:{1:"\\left\\lceil${args[0]}\\right\\rceil"},cube:{1:"\\left(${args[0]}\\right)^3"},divide:{2:"\\frac{${args[0]}}{${args[1]}}"},dotDivide:{2:"\\left(${args[0]}".concat(of.dotDivide,"${args[1]}\\right)")},dotMultiply:{2:"\\left(${args[0]}".concat(of.dotMultiply,"${args[1]}\\right)")},dotPow:{2:"\\left(${args[0]}".concat(of.dotPow,"${args[1]}\\right)")},exp:{1:"\\exp\\left(${args[0]}\\right)"},expm1:"\\left(e".concat(of.pow,"{${args[0]}}-1\\right)"),fix:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},floor:{1:"\\left\\lfloor${args[0]}\\right\\rfloor"},gcd:"\\gcd\\left(${args}\\right)",hypot:"\\hypot\\left(${args}\\right)",log:{1:"\\ln\\left(${args[0]}\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}\\right)"},log10:{1:"\\log_{10}\\left(${args[0]}\\right)"},log1p:{1:"\\ln\\left(${args[0]}+1\\right)",2:"\\log_{${args[1]}}\\left(${args[0]}+1\\right)"},log2:"\\log_{2}\\left(${args[0]}\\right)",mod:{2:"\\left(${args[0]}".concat(of.mod,"${args[1]}\\right)")},multiply:{2:"\\left(${args[0]}".concat(of.multiply,"${args[1]}\\right)")},norm:{1:"\\left\\|${args[0]}\\right\\|",2:void 0},nthRoot:{2:"\\sqrt[${args[1]}]{${args[0]}}"},nthRoots:{2:"\\{y : $y^{args[1]} = {${args[0]}}\\}"},pow:{2:"\\left(${args[0]}\\right)".concat(of.pow,"{${args[1]}}")},round:{1:"\\left\\lfloor${args[0]}\\right\\rceil",2:void 0},sign:{1:"\\mathrm{${name}}\\left(${args[0]}\\right)"},sqrt:{1:"\\sqrt{${args[0]}}"},square:{1:"\\left(${args[0]}\\right)^2"},subtract:{2:"\\left(${args[0]}".concat(of.subtract,"${args[1]}\\right)")},unaryMinus:{1:"".concat(of.unaryMinus,"\\left(${args[0]}\\right)")},unaryPlus:{1:"".concat(of.unaryPlus,"\\left(${args[0]}\\right)")},bitAnd:{2:"\\left(${args[0]}".concat(of.bitAnd,"${args[1]}\\right)")},bitNot:{1:of.bitNot+"\\left(${args[0]}\\right)"},bitOr:{2:"\\left(${args[0]}".concat(of.bitOr,"${args[1]}\\right)")},bitXor:{2:"\\left(${args[0]}".concat(of.bitXor,"${args[1]}\\right)")},leftShift:{2:"\\left(${args[0]}".concat(of.leftShift,"${args[1]}\\right)")},rightArithShift:{2:"\\left(${args[0]}".concat(of.rightArithShift,"${args[1]}\\right)")},rightLogShift:{2:"\\left(${args[0]}".concat(of.rightLogShift,"${args[1]}\\right)")},bellNumbers:{1:"\\mathrm{B}_{${args[0]}}"},catalan:{1:"\\mathrm{C}_{${args[0]}}"},stirlingS2:{2:"\\mathrm{S}\\left(${args}\\right)"},arg:{1:"\\arg\\left(${args[0]}\\right)"},conj:{1:"\\left(${args[0]}\\right)^*"},im:{1:"\\Im\\left\\lbrace${args[0]}\\right\\rbrace"},re:{1:"\\Re\\left\\lbrace${args[0]}\\right\\rbrace"},and:{2:"\\left(${args[0]}".concat(of.and,"${args[1]}\\right)")},not:{1:of.not+"\\left(${args[0]}\\right)"},or:{2:"\\left(${args[0]}".concat(of.or,"${args[1]}\\right)")},xor:{2:"\\left(${args[0]}".concat(of.xor,"${args[1]}\\right)")},cross:{2:"\\left(${args[0]}\\right)\\times\\left(${args[1]}\\right)"},ctranspose:{1:"\\left(${args[0]}\\right)".concat(of.ctranspose)},det:{1:"\\det\\left(${args[0]}\\right)"},dot:{2:"\\left(${args[0]}\\cdot${args[1]}\\right)"},expm:{1:"\\exp\\left(${args[0]}\\right)"},inv:{1:"\\left(${args[0]}\\right)^{-1}"},sqrtm:{1:"{${args[0]}}".concat(of.pow,"{\\frac{1}{2}}")},trace:{1:"\\mathrm{tr}\\left(${args[0]}\\right)"},transpose:{1:"\\left(${args[0]}\\right)".concat(of.transpose)},combinations:{2:"\\binom{${args[0]}}{${args[1]}}"},combinationsWithRep:{2:"\\left(\\!\\!{\\binom{${args[0]}}{${args[1]}}}\\!\\!\\right)"},factorial:{1:"\\left(${args[0]}\\right)".concat(of.factorial)},gamma:{1:"\\Gamma\\left(${args[0]}\\right)"},equal:{2:"\\left(${args[0]}".concat(of.equal,"${args[1]}\\right)")},larger:{2:"\\left(${args[0]}".concat(of.larger,"${args[1]}\\right)")},largerEq:{2:"\\left(${args[0]}".concat(of.largerEq,"${args[1]}\\right)")},smaller:{2:"\\left(${args[0]}".concat(of.smaller,"${args[1]}\\right)")},smallerEq:{2:"\\left(${args[0]}".concat(of.smallerEq,"${args[1]}\\right)")},unequal:{2:"\\left(${args[0]}".concat(of.unequal,"${args[1]}\\right)")},erf:{1:"erf\\left(${args[0]}\\right)"},max:"\\max\\left(${args}\\right)",min:"\\min\\left(${args}\\right)",variance:"\\mathrm{Var}\\left(${args}\\right)",acos:{1:"\\cos^{-1}\\left(${args[0]}\\right)"},acosh:{1:"\\cosh^{-1}\\left(${args[0]}\\right)"},acot:{1:"\\cot^{-1}\\left(${args[0]}\\right)"},acoth:{1:"\\coth^{-1}\\left(${args[0]}\\right)"},acsc:{1:"\\csc^{-1}\\left(${args[0]}\\right)"},acsch:{1:"\\mathrm{csch}^{-1}\\left(${args[0]}\\right)"},asec:{1:"\\sec^{-1}\\left(${args[0]}\\right)"},asech:{1:"\\mathrm{sech}^{-1}\\left(${args[0]}\\right)"},asin:{1:"\\sin^{-1}\\left(${args[0]}\\right)"},asinh:{1:"\\sinh^{-1}\\left(${args[0]}\\right)"},atan:{1:"\\tan^{-1}\\left(${args[0]}\\right)"},atan2:{2:"\\mathrm{atan2}\\left(${args}\\right)"},atanh:{1:"\\tanh^{-1}\\left(${args[0]}\\right)"},cos:{1:"\\cos\\left(${args[0]}\\right)"},cosh:{1:"\\cosh\\left(${args[0]}\\right)"},cot:{1:"\\cot\\left(${args[0]}\\right)"},coth:{1:"\\coth\\left(${args[0]}\\right)"},csc:{1:"\\csc\\left(${args[0]}\\right)"},csch:{1:"\\mathrm{csch}\\left(${args[0]}\\right)"},sec:{1:"\\sec\\left(${args[0]}\\right)"},sech:{1:"\\mathrm{sech}\\left(${args[0]}\\right)"},sin:{1:"\\sin\\left(${args[0]}\\right)"},sinh:{1:"\\sinh\\left(${args[0]}\\right)"},tan:{1:"\\tan\\left(${args[0]}\\right)"},tanh:{1:"\\tanh\\left(${args[0]}\\right)"},to:{2:"\\left(${args[0]}".concat(of.to,"${args[1]}\\right)")},numeric:function(e,t){return e.args[0].toTex()},number:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"},string:{0:'\\mathtt{""}',1:"\\mathrm{string}\\left(${args[0]}\\right)"},bignumber:{0:"0",1:"\\left(${args[0]}\\right)"},complex:{0:"0",1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)+".concat(af.i,"\\cdot\\left(${args[1]}\\right)\\right)")},matrix:{0:"\\begin{bmatrix}\\end{bmatrix}",1:"\\left(${args[0]}\\right)",2:"\\left(${args[0]}\\right)"},sparse:{0:"\\begin{bsparse}\\end{bsparse}",1:"\\left(${args[0]}\\right)"},unit:{1:"\\left(${args[0]}\\right)",2:"\\left(\\left(${args[0]}\\right)${args[1]}\\right)"}},uf={deg:"^\\circ"};function cf(e){return nf()(e,{preserveFormatting:!0})}function ff(e,t){return(t=void 0!==t&&t)?Object(ae.f)(uf,e)?uf[e]:"\\mathrm{"+cf(e)+"}":Object(ae.f)(af,e)?af[e]:cf(e)}var lf=["Node"],pf=Object(s.a)("ConstantNode",lf,function(e){var t=e.Node;function r(e){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");if(2===arguments.length)throw new SyntaxError("new ConstantNode(valueStr, valueType) is not supported anymore since math v4.0.0. Use new ConstantNode(value) instead, where value is a non-stringified value.");this.value=e}return(r.prototype=new t).type="ConstantNode",r.prototype.isConstantNode=!0,r.prototype._compile=function(e,t){var r=this.value;return function(){return r}},r.prototype.forEach=function(e){},r.prototype.map=function(e){return this.clone()},r.prototype.clone=function(){return new r(this.value)},r.prototype._toString=function(e){return Object(J.d)(this.value,e)},r.prototype.toHTML=function(e){var t=this._toString(e);switch(Object(ie.M)(this.value)){case"number":case"BigNumber":case"Fraction":return'<span class="math-number">'+t+"</span>";case"string":return'<span class="math-string">'+t+"</span>";case"boolean":return'<span class="math-boolean">'+t+"</span>";case"null":return'<span class="math-null-symbol">'+t+"</span>";case"undefined":return'<span class="math-undefined">'+t+"</span>";default:return'<span class="math-symbol">'+t+"</span>"}},r.prototype.toJSON=function(){return{mathjs:"ConstantNode",value:this.value}},r.fromJSON=function(e){return new r(e.value)},r.prototype._toTex=function(e){var t=this._toString(e);switch(Object(ie.M)(this.value)){case"string":return"\\mathtt{"+cf(t)+"}";case"number":case"BigNumber":var r=t.toLowerCase().indexOf("e");return-1!==r?t.substring(0,r)+"\\cdot10^{"+t.substring(r+1)+"}":t;case"Fraction":return this.value.toLatex();default:return t}},r},{isClass:!0,isNode:!0}),mf=["typed","Node"],hf=Object(s.a)("FunctionAssignmentNode",mf,function(e){var f=e.typed,t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if("string"!=typeof e)throw new TypeError('String expected for parameter "name"');if(!Array.isArray(t))throw new TypeError('Array containing strings or objects expected for parameter "params"');if(!Object(ie.w)(r))throw new TypeError('Node expected for parameter "expr"');if(e in Bc)throw new Error('Illegal function name, "'+e+'" is a reserved keyword');this.name=e,this.params=t.map(function(e){return e&&e.name||e}),this.types=t.map(function(e){return e&&e.type||"any"}),this.expr=r}function a(e,t){var r=Vc(e,t),n=Vc(e.expr,t);return"all"===t||null!==n&&n<=r}return(n.prototype=new t).type="FunctionAssignmentNode",n.prototype.isFunctionAssignmentNode=!0,n.prototype._compile=function(e,t){var r=Object.create(t);Object(I.f)(this.params,function(e){r[e]=!0});var a=this.expr._compile(e,r),o=this.name,s=this.params,u=Object(I.k)(this.types,","),c=o+"("+Object(I.k)(this.params,", ")+")";return function(r,n,i){var e={};e[u]=function(){for(var e=Object.create(n),t=0;t<s.length;t++)e[s[t]]=arguments[t];return a(r,e,i)};var t=f(o,e);return t.syntax=c,Ui(r,o,t),t}},n.prototype.forEach=function(e){e(this.expr,"expr",this)},n.prototype.map=function(e){var t=this._ifNode(e(this.expr,"expr",this));return new n(this.name,this.params.slice(0),t)},n.prototype.clone=function(){return new n(this.name,this.params.slice(0),this.expr)},n.prototype._toString=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=this.expr.toString(e);return a(this,t)&&(r="("+r+")"),this.name+"("+this.params.join(", ")+") = "+r},n.prototype.toJSON=function(){var r=this.types;return{mathjs:"FunctionAssignmentNode",name:this.name,params:this.params.map(function(e,t){return{name:e,type:r[t]}}),expr:this.expr}},n.fromJSON=function(e){return new n(e.name,e.params,e.expr)},n.prototype.toHTML=function(e){for(var t=e&&e.parenthesis?e.parenthesis:"keep",r=[],n=0;n<this.params.length;n++)r.push('<span class="math-symbol math-parameter">'+Object(J.c)(this.params[n])+"</span>");var i=this.expr.toHTML(e);return a(this,t)&&(i='<span class="math-parenthesis math-round-parenthesis">(</span>'+i+'<span class="math-parenthesis math-round-parenthesis">)</span>'),'<span class="math-function">'+Object(J.c)(this.name)+'</span><span class="math-parenthesis math-round-parenthesis">(</span>'+r.join('<span class="math-separator">,</span>')+'<span class="math-parenthesis math-round-parenthesis">)</span><span class="math-operator math-assignment-operator math-variable-assignment-operator math-binary-operator">=</span>'+i},n.prototype._toTex=function(e){var t=e&&e.parenthesis?e.parenthesis:"keep",r=this.expr.toTex(e);return a(this,t)&&(r="\\left(".concat(r,"\\right)")),"\\mathrm{"+this.name+"}\\left("+this.params.map(ff).join(",")+"\\right):="+r},n},{isClass:!0,isNode:!0}),df=["Index"],yf=Object(s.a)("index",df,function(e){var a=e.Index;return function(){for(var e=[],t=0,r=arguments.length;t<r;t++){var n=arguments[t];if(Object(ie.D)(n))n.start--,n.end-=0<n.step?0:2;else if(n&&!0===n.isSet)n=n.map(function(e){return e-1});else if(Object(ie.b)(n)||Object(ie.v)(n))n=n.map(function(e){return e-1});else if(Object(ie.y)(n))n--;else if(Object(ie.e)(n))n=n.toNumber()-1;else if("string"!=typeof n)throw new TypeError("Dimension must be an Array, Matrix, number, string, or Range");e[t]=n}var i=new a;return a.apply(i,e),i}},{isTransformFunction:!0});function gf(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}var vf=["Range","Node","Index","size"],bf=Object(s.a)("IndexNode",vf,function(e){var n=e.Range,t=e.Node,r=e.Index,h=e.size,a=yf({Index:r});function i(e,t){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if(this.dimensions=e,this.dotNotation=t||!1,!Array.isArray(e)||!e.every(ie.w))throw new TypeError('Array containing Nodes expected for parameter "dimensions"');if(this.dotNotation&&!this.isObjectProperty())throw new Error("dotNotation only applicable for object properties");function r(){throw new Error("Property `IndexNode.object` is deprecated, use `IndexNode.fn` instead")}Object.defineProperty(this,"object",{get:r,set:r})}function d(e,t,r){return new n(Object(ie.e)(e)?e.toNumber():e,Object(ie.e)(t)?t.toNumber():t,Object(ie.e)(r)?r.toNumber():r)}return(i.prototype=new t).type="IndexNode",i.prototype.isIndexNode=!0,i.prototype._compile=function(p,m){var i=Object(I.m)(this.dimensions,function(e,a){if(Object(ie.E)(e)){if(e.needsEnd()){var t=Object.create(m);t.end=!0;var o=e.start._compile(p,t),s=e.end._compile(p,t),u=e.step?e.step._compile(p,t):function(){return 1};return function(e,t,r){var n=h(r).valueOf(),i=Object.create(t);return i.end=n[a],d(o(e,i,r),s(e,i,r),u(e,i,r))}}var n=e.start._compile(p,m),i=e.end._compile(p,m),c=e.step?e.step._compile(p,m):function(){return 1};return function(e,t,r){return d(n(e,t,r),i(e,t,r),c(e,t,r))}}if(Object(ie.J)(e)&&"end"===e.name){var r=Object.create(m);r.end=!0;var f=e._compile(p,r);return function(e,t,r){var n=h(r).valueOf(),i=Object.create(t);return i.end=n[a],f(e,i,r)}}var l=e._compile(p,m);return function(e,t,r){return l(e,t,r)}});return function(t,r,n){var e=Object(I.m)(i,function(e){return e(t,r,n)});return a.apply(void 0,gf(e))}},i.prototype.forEach=function(e){for(var t=0;t<this.dimensions.length;t++)e(this.dimensions[t],"dimensions["+t+"]",this)},i.prototype.map=function(e){for(var t=[],r=0;r<this.dimensions.length;r++)t[r]=this._ifNode(e(this.dimensions[r],"dimensions["+r+"]",this));return new i(t,this.dotNotation)},i.prototype.clone=function(){return new i(this.dimensions.slice(0),this.dotNotation)},i.prototype.isObjectProperty=function(){return 1===this.dimensions.length&&Object(ie.l)(this.dimensions[0])&&"string"==typeof this.dimensions[0].value},i.prototype.getObjectProperty=function(){return this.isObjectProperty()?this.dimensions[0].value:null},i.prototype._toString=function(e){return this.dotNotation?"."+this.getObjectProperty():"["+this.dimensions.join(", ")+"]"},i.prototype.toJSON=function(){return{mathjs:"IndexNode",dimensions:this.dimensions,dotNotation:this.dotNotation}},i.fromJSON=function(e){return new i(e.dimensions,e.dotNotation)},i.prototype.toHTML=function(e){for(var t=[],r=0;r<this.dimensions.length;r++)t[r]=this.dimensions[r].toHTML();return this.dotNotation?'<span class="math-operator math-accessor-operator">.</span><span class="math-symbol math-property">'+Object(J.c)(this.getObjectProperty())+"</span>":'<span class="math-parenthesis math-square-parenthesis">[</span>'+t.join('<span class="math-separator">,</span>')+'<span class="math-parenthesis math-square-parenthesis">]</span>'},i.prototype._toTex=function(t){var e=this.dimensions.map(function(e){return e.toTex(t)});return this.dotNotation?"."+this.getObjectProperty():"_{"+e.join(",")+"}"},i},{isClass:!0,isNode:!0});function xf(e){return(xf="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var wf=["Node"],Nf=Object(s.a)("ObjectNode",wf,function(e){var t=e.Node;function n(t){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(this.properties=t||{},t&&("object"!==xf(t)||!Object.keys(t).every(function(e){return Object(ie.w)(t[e])})))throw new TypeError("Object containing Nodes expected")}return(n.prototype=new t).type="ObjectNode",n.prototype.isObjectNode=!0,n.prototype._compile=function(e,t){var a={};for(var r in this.properties)if(Object(ae.f)(this.properties,r)){var n=Object(J.e)(r),i=JSON.parse(n);if(!Li(this.properties,i))throw new Error('No access to property "'+i+'"');a[i]=this.properties[r]._compile(e,t)}return function(e,t,r){var n={};for(var i in a)Object(ae.f)(a,i)&&(n[i]=a[i](e,t,r));return n}},n.prototype.forEach=function(e){for(var t in this.properties)Object(ae.f)(this.properties,t)&&e(this.properties[t],"properties["+Object(J.e)(t)+"]",this)},n.prototype.map=function(e){var t={};for(var r in this.properties)Object(ae.f)(this.properties,r)&&(t[r]=this._ifNode(e(this.properties[r],"properties["+Object(J.e)(r)+"]",this)));return new n(t)},n.prototype.clone=function(){var e={};for(var t in this.properties)Object(ae.f)(this.properties,t)&&(e[t]=this.properties[t]);return new n(e)},n.prototype._toString=function(e){var t=[];for(var r in this.properties)Object(ae.f)(this.properties,r)&&t.push(Object(J.e)(r)+": "+this.properties[r].toString(e));return"{"+t.join(", ")+"}"},n.prototype.toJSON=function(){return{mathjs:"ObjectNode",properties:this.properties}},n.fromJSON=function(e){return new n(e.properties)},n.prototype.toHTML=function(e){var t=[];for(var r in this.properties)Object(ae.f)(this.properties,r)&&t.push('<span class="math-symbol math-property">'+Object(J.c)(r)+'</span><span class="math-operator math-assignment-operator math-property-assignment-operator math-binary-operator">:</span>'+this.properties[r].toHTML(e));return'<span class="math-parenthesis math-curly-parenthesis">{</span>'+t.join('<span class="math-separator">,</span>')+'<span class="math-parenthesis math-curly-parenthesis">}</span>'},n.prototype._toTex=function(e){var t=[];for(var r in this.properties)Object(ae.f)(this.properties,r)&&t.push("\\mathbf{"+r+":} & "+this.properties[r].toTex(e)+"\\\\");return"\\left\\{\\begin{array}{ll}".concat(t.join("\n"),"\\end{array}\\right\\}")},n},{isClass:!0,isNode:!0}),Of=["Node"],Mf=Object(s.a)("OperatorNode",Of,function(e){var t=e.Node;function i(e,t,r,n){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if("string"!=typeof e)throw new TypeError('string expected for parameter "op"');if("string"!=typeof t)throw new TypeError('string expected for parameter "fn"');if(!Array.isArray(r)||!r.every(ie.w))throw new TypeError('Array containing Nodes expected for parameter "args"');this.implicit=!0===n,this.op=e,this.fn=t,this.args=r||[]}function m(i,a,e,t,r){var n,o=Vc(i,a),s=Jc(i,a);if("all"===a||2<t.length&&"OperatorNode:add"!==i.getIdentifier()&&"OperatorNode:multiply"!==i.getIdentifier())return t.map(function(e){switch(e.getContent().type){case"ArrayNode":case"ConstantNode":case"SymbolNode":case"ParenthesisNode":return!1;default:return!0}});switch(t.length){case 0:n=[];break;case 1:var u=Vc(t[0],a);if(r&&null!==u){var c,f;if(f="keep"===a?(c=t[0].getIdentifier(),i.getIdentifier()):(c=t[0].getContent().getIdentifier(),i.getContent().getIdentifier()),!1===Zc[o][f].latexLeftParens){n=[!1];break}if(!1===Zc[u][c].latexParens){n=[!1];break}}if(null===u){n=[!1];break}if(u<=o){n=[!0];break}n=[!1];break;case 2:var l,p,m=Vc(t[0],a),h=Wc(i,t[0],a);l=null!==m&&(m===o&&"right"===s&&!h||m<o);var d,y,g,v=Vc(t[1],a),b=Wc(i,t[1],a);if(p=null!==v&&(v===o&&"left"===s&&!b||v<o),r)g="keep"===a?(d=i.getIdentifier(),y=i.args[0].getIdentifier(),i.args[1].getIdentifier()):(d=i.getContent().getIdentifier(),y=i.args[0].getContent().getIdentifier(),i.args[1].getContent().getIdentifier()),null!==m&&(!1===Zc[o][d].latexLeftParens&&(l=!1),!1===Zc[m][y].latexParens&&(l=!1)),null!==v&&(!1===Zc[o][d].latexRightParens&&(p=!1),!1===Zc[v][g].latexParens&&(p=!1));n=[l,p];break;default:"OperatorNode:add"!==i.getIdentifier()&&"OperatorNode:multiply"!==i.getIdentifier()||(n=t.map(function(e){var t=Vc(e,a),r=Wc(i,e,a),n=Jc(e,a);return null!==t&&(o===t&&s===n&&!r||t<o)}))}return 2<=t.length&&"OperatorNode:multiply"===i.getIdentifier()&&i.implicit&&"auto"===a&&"hide"===e&&(n=t.map(function(e,t){var r="ParenthesisNode"===e.getIdentifier();return!(!n[t]&&!r)})),n}return(i.prototype=new t).type="OperatorNode",i.prototype.isOperatorNode=!0,i.prototype._compile=function(t,r){if("string"!=typeof this.fn||!Hi(t,this.fn))throw t[this.fn]?new Error('No access to function "'+this.fn+'"'):new Error("Function "+this.fn+' missing in provided namespace "math"');var i=Fi(t,this.fn),e=Object(I.m)(this.args,function(e){return e._compile(t,r)});if(1===e.length){var n=e[0];return function(e,t,r){return i(n(e,t,r))}}if(2!==e.length)return function(t,r,n){return i.apply(null,Object(I.m)(e,function(e){return e(t,r,n)}))};var a=e[0],o=e[1];return function(e,t,r){return i(a(e,t,r),o(e,t,r))}},i.prototype.forEach=function(e){for(var t=0;t<this.args.length;t++)e(this.args[t],"args["+t+"]",this)},i.prototype.map=function(e){for(var t=[],r=0;r<this.args.length;r++)t[r]=this._ifNode(e(this.args[r],"args["+r+"]",this));return new i(this.op,this.fn,t,this.implicit)},i.prototype.clone=function(){return new i(this.op,this.fn,this.args.slice(0),this.implicit)},i.prototype.isUnary=function(){return 1===this.args.length},i.prototype.isBinary=function(){return 2===this.args.length},i.prototype._toString=function(r){var e=r&&r.parenthesis?r.parenthesis:"keep",t=r&&r.implicit?r.implicit:"hide",n=this.args,i=m(this,e,t,n,!1);if(1===n.length){var a=Jc(this,e),o=n[0].toString(r);i[0]&&(o="("+o+")");var s=/[a-zA-Z]+/.test(this.op);return"right"===a?this.op+(s?" ":"")+o:"left"===a?o+(s?" ":"")+this.op:o+this.op}if(2===n.length){var u=n[0].toString(r),c=n[1].toString(r);return i[0]&&(u="("+u+")"),i[1]&&(c="("+c+")"),this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===t?u+" "+c:u+" "+this.op+" "+c}if(2<n.length&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())){var f=n.map(function(e,t){return e=e.toString(r),i[t]&&(e="("+e+")"),e});return this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===t?f.join(" "):f.join(" "+this.op+" ")}return this.fn+"("+this.args.join(", ")+")"},i.prototype.toJSON=function(){return{mathjs:"OperatorNode",op:this.op,fn:this.fn,args:this.args,implicit:this.implicit}},i.fromJSON=function(e){return new i(e.op,e.fn,e.args,e.implicit)},i.prototype.toHTML=function(r){var e=r&&r.parenthesis?r.parenthesis:"keep",t=r&&r.implicit?r.implicit:"hide",n=this.args,i=m(this,e,t,n,!1);if(1===n.length){var a=Jc(this,e),o=n[0].toHTML(r);return i[0]&&(o='<span class="math-parenthesis math-round-parenthesis">(</span>'+o+'<span class="math-parenthesis math-round-parenthesis">)</span>'),"right"===a?'<span class="math-operator math-unary-operator math-lefthand-unary-operator">'+Object(J.c)(this.op)+"</span>"+o:o+'<span class="math-operator math-unary-operator math-righthand-unary-operator">'+Object(J.c)(this.op)+"</span>"}if(2===n.length){var s=n[0].toHTML(r),u=n[1].toHTML(r);return i[0]&&(s='<span class="math-parenthesis math-round-parenthesis">(</span>'+s+'<span class="math-parenthesis math-round-parenthesis">)</span>'),i[1]&&(u='<span class="math-parenthesis math-round-parenthesis">(</span>'+u+'<span class="math-parenthesis math-round-parenthesis">)</span>'),this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===t?s+'<span class="math-operator math-binary-operator math-implicit-binary-operator"></span>'+u:s+'<span class="math-operator math-binary-operator math-explicit-binary-operator">'+Object(J.c)(this.op)+"</span>"+u}var c=n.map(function(e,t){return e=e.toHTML(r),i[t]&&(e='<span class="math-parenthesis math-round-parenthesis">(</span>'+e+'<span class="math-parenthesis math-round-parenthesis">)</span>'),e});return 2<n.length&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())?this.implicit&&"OperatorNode:multiply"===this.getIdentifier()&&"hide"===t?c.join('<span class="math-operator math-binary-operator math-implicit-binary-operator"></span>'):c.join('<span class="math-operator math-binary-operator math-explicit-binary-operator">'+Object(J.c)(this.op)+"</span>"):'<span class="math-function">'+Object(J.c)(this.fn)+'</span><span class="math-paranthesis math-round-parenthesis">(</span>'+c.join('<span class="math-separator">,</span>')+'<span class="math-paranthesis math-round-parenthesis">)</span>'},i.prototype._toTex=function(r){var e=r&&r.parenthesis?r.parenthesis:"keep",t=r&&r.implicit?r.implicit:"hide",n=this.args,i=m(this,e,t,n,!0),a=of[this.fn];if(a=void 0===a?this.op:a,1===n.length){var o=Jc(this,e),s=n[0].toTex(r);return i[0]&&(s="\\left(".concat(s,"\\right)")),"right"===o?a+s:s+a}if(2===n.length){var u=n[0],c=u.toTex(r);i[0]&&(c="\\left(".concat(c,"\\right)"));var f,l=n[1].toTex(r);switch(i[1]&&(l="\\left(".concat(l,"\\right)")),f="keep"===e?u.getIdentifier():u.getContent().getIdentifier(),this.getIdentifier()){case"OperatorNode:divide":return a+"{"+c+"}{"+l+"}";case"OperatorNode:pow":switch(c="{"+c+"}",l="{"+l+"}",f){case"ConditionalNode":case"OperatorNode:divide":c="\\left(".concat(c,"\\right)")}break;case"OperatorNode:multiply":if(this.implicit&&"hide"===t)return c+"~"+l}return c+a+l}if(2<n.length&&("OperatorNode:add"===this.getIdentifier()||"OperatorNode:multiply"===this.getIdentifier())){var p=n.map(function(e,t){return e=e.toTex(r),i[t]&&(e="\\left(".concat(e,"\\right)")),e});return"OperatorNode:multiply"===this.getIdentifier()&&this.implicit?p.join("~"):p.join(a)}return"\\mathrm{"+this.fn+"}\\left("+n.map(function(e){return e.toTex(r)}).join(",")+"\\right)"},i.prototype.getIdentifier=function(){return this.type+":"+this.fn},i},{isClass:!0,isNode:!0}),Ef=["Node"],jf=Object(s.a)("ParenthesisNode",Ef,function(e){var t=e.Node;function r(e){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError('Node expected for parameter "content"');this.content=e}return(r.prototype=new t).type="ParenthesisNode",r.prototype.isParenthesisNode=!0,r.prototype._compile=function(e,t){return this.content._compile(e,t)},r.prototype.getContent=function(){return this.content.getContent()},r.prototype.forEach=function(e){e(this.content,"content",this)},r.prototype.map=function(e){return new r(e(this.content,"content",this))},r.prototype.clone=function(){return new r(this.content)},r.prototype._toString=function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?"("+this.content.toString(e)+")":this.content.toString(e)},r.prototype.toJSON=function(){return{mathjs:"ParenthesisNode",content:this.content}},r.fromJSON=function(e){return new r(e.content)},r.prototype.toHTML=function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?'<span class="math-parenthesis math-round-parenthesis">(</span>'+this.content.toHTML(e)+'<span class="math-parenthesis math-round-parenthesis">)</span>':this.content.toHTML(e)},r.prototype._toTex=function(e){return!e||e&&!e.parenthesis||e&&"keep"===e.parenthesis?"\\left(".concat(this.content.toTex(e),"\\right)"):this.content.toTex(e)},r},{isClass:!0,isNode:!0}),Sf=["Node"],Af=Object(s.a)("RangeNode",Sf,function(e){var t=e.Node;function n(e,t,r){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!Object(ie.w)(e))throw new TypeError("Node expected");if(!Object(ie.w)(t))throw new TypeError("Node expected");if(r&&!Object(ie.w)(r))throw new TypeError("Node expected");if(3<arguments.length)throw new Error("Too many arguments");this.start=e,this.end=t,this.step=r||null}function o(e,t){var r=Vc(e,t),n={},i=Vc(e.start,t);if(n.start=null!==i&&i<=r||"all"===t,e.step){var a=Vc(e.step,t);n.step=null!==a&&a<=r||"all"===t}var o=Vc(e.end,t);return n.end=null!==o&&o<=r||"all"===t,n}return(n.prototype=new t).type="RangeNode",n.prototype.isRangeNode=!0,n.prototype.needsEnd=function(){return 0<this.filter(function(e){return Object(ie.J)(e)&&"end"===e.name}).length},n.prototype._compile=function(e,t){var n=e.range,i=this.start._compile(e,t),a=this.end._compile(e,t);if(this.step){var o=this.step._compile(e,t);return function(e,t,r){return n(i(e,t,r),a(e,t,r),o(e,t,r))}}return function(e,t,r){return n(i(e,t,r),a(e,t,r))}},n.prototype.forEach=function(e){e(this.start,"start",this),e(this.end,"end",this),this.step&&e(this.step,"step",this)},n.prototype.map=function(e){return new n(this._ifNode(e(this.start,"start",this)),this._ifNode(e(this.end,"end",this)),this.step&&this._ifNode(e(this.step,"step",this)))},n.prototype.clone=function(){return new n(this.start,this.end,this.step&&this.step)},n.prototype._toString=function(e){var t,r=o(this,e&&e.parenthesis?e.parenthesis:"keep"),n=this.start.toString(e);if(r.start&&(n="("+n+")"),t=n,this.step){var i=this.step.toString(e);r.step&&(i="("+i+")"),t+=":"+i}var a=this.end.toString(e);return r.end&&(a="("+a+")"),t+=":"+a},n.prototype.toJSON=function(){return{mathjs:"RangeNode",start:this.start,end:this.end,step:this.step}},n.fromJSON=function(e){return new n(e.start,e.end,e.step)},n.prototype.toHTML=function(e){var t,r=o(this,e&&e.parenthesis?e.parenthesis:"keep"),n=this.start.toHTML(e);if(r.start&&(n='<span class="math-parenthesis math-round-parenthesis">(</span>'+n+'<span class="math-parenthesis math-round-parenthesis">)</span>'),t=n,this.step){var i=this.step.toHTML(e);r.step&&(i='<span class="math-parenthesis math-round-parenthesis">(</span>'+i+'<span class="math-parenthesis math-round-parenthesis">)</span>'),t+='<span class="math-operator math-range-operator">:</span>'+i}var a=this.end.toHTML(e);return r.end&&(a='<span class="math-parenthesis math-round-parenthesis">(</span>'+a+'<span class="math-parenthesis math-round-parenthesis">)</span>'),t+='<span class="math-operator math-range-operator">:</span>'+a},n.prototype._toTex=function(e){var t=o(this,e&&e.parenthesis?e.parenthesis:"keep"),r=this.start.toTex(e);if(t.start&&(r="\\left(".concat(r,"\\right)")),this.step){var n=this.step.toTex(e);t.step&&(n="\\left(".concat(n,"\\right)")),r+=":"+n}var i=this.end.toTex(e);return t.end&&(i="\\left(".concat(i,"\\right)")),r+=":"+i},n},{isClass:!0,isNode:!0}),Cf=["Node"],Tf=Object(s.a)("RelationalNode",Cf,function(e){var t=e.Node;function i(e,t){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");if(!Array.isArray(e))throw new TypeError("Parameter conditionals must be an array");if(!Array.isArray(t))throw new TypeError("Parameter params must be an array");if(e.length!==t.length-1)throw new TypeError("Parameter params must contain exactly one more element than parameter conditionals");this.conditionals=e,this.params=t}return(i.prototype=new t).type="RelationalNode",i.prototype.isRelationalNode=!0,i.prototype._compile=function(o,t){var s=this,u=this.params.map(function(e){return e._compile(o,t)});return function(e,t,r){for(var n,i=u[0](e,t,r),a=0;a<s.conditionals.length;a++){if(n=i,i=u[a+1](e,t,r),!Fi(o,s.conditionals[a])(n,i))return!1}return!0}},i.prototype.forEach=function(r){var n=this;this.params.forEach(function(e,t){return r(e,"params["+t+"]",n)},this)},i.prototype.map=function(r){var n=this;return new i(this.conditionals.slice(),this.params.map(function(e,t){return n._ifNode(r(e,"params["+t+"]",n))},this))},i.prototype.clone=function(){return new i(this.conditionals,this.params)},i.prototype._toString=function(n){for(var i=n&&n.parenthesis?n.parenthesis:"keep",a=Vc(this,i),e=this.params.map(function(e,t){var r=Vc(e,i);return"all"===i||null!==r&&r<=a?"("+e.toString(n)+")":e.toString(n)}),t={equal:"==",unequal:"!=",smaller:"<",larger:">",smallerEq:"<=",largerEq:">="},r=e[0],o=0;o<this.conditionals.length;o++)r+=" "+t[this.conditionals[o]]+" "+e[o+1];return r},i.prototype.toJSON=function(){return{mathjs:"RelationalNode",conditionals:this.conditionals,params:this.params}},i.fromJSON=function(e){return new i(e.conditionals,e.params)},i.prototype.toHTML=function(n){for(var i=n&&n.parenthesis?n.parenthesis:"keep",a=Vc(this,i),e=this.params.map(function(e,t){var r=Vc(e,i);return"all"===i||null!==r&&r<=a?'<span class="math-parenthesis math-round-parenthesis">(</span>'+e.toHTML(n)+'<span class="math-parenthesis math-round-parenthesis">)</span>':e.toHTML(n)}),t={equal:"==",unequal:"!=",smaller:"<",larger:">",smallerEq:"<=",largerEq:">="},r=e[0],o=0;o<this.conditionals.length;o++)r+='<span class="math-operator math-binary-operator math-explicit-binary-operator">'+Object(J.c)(t[this.conditionals[o]])+"</span>"+e[o+1];return r},i.prototype._toTex=function(n){for(var i=n&&n.parenthesis?n.parenthesis:"keep",a=Vc(this,i),e=this.params.map(function(e,t){var r=Vc(e,i);return"all"===i||null!==r&&r<=a?"\\left("+e.toTex(n)+"\right)":e.toTex(n)}),t=e[0],r=0;r<this.conditionals.length;r++)t+=of[this.conditionals[r]]+e[r+1];return t},i},{isClass:!0,isNode:!0}),_f=["math","?Unit","Node"],If=Object(s.a)("SymbolNode",_f,function(e){var n=e.math,o=e.Unit,t=e.Node;function s(e){return!!o&&o.isValuelessUnit(e)}function r(e){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");if("string"!=typeof e)throw new TypeError('String expected for parameter "name"');this.name=e}return(r.prototype=new t).type="SymbolNode",r.prototype.isSymbolNode=!0,r.prototype._compile=function(n,e){var i=this.name;if(!0===e[i])return function(e,t,r){return t[i]};if(i in n)return function(e,t,r){return Fi(i in e?e:n,i)};var a=s(i);return function(e,t,r){return i in e?Fi(e,i):a?new o(null,i):function(e){throw new Error("Undefined symbol "+e)}(i)}},r.prototype.forEach=function(e){},r.prototype.map=function(e){return this.clone()},r.prototype.clone=function(){return new r(this.name)},r.prototype._toString=function(e){return this.name},r.prototype.toHTML=function(e){var t=Object(J.c)(this.name);return"true"===t||"false"===t?'<span class="math-symbol math-boolean">'+t+"</span>":"i"===t?'<span class="math-symbol math-imaginary-symbol">'+t+"</span>":"Infinity"===t?'<span class="math-symbol math-infinity-symbol">'+t+"</span>":"NaN"===t?'<span class="math-symbol math-nan-symbol">'+t+"</span>":"null"===t?'<span class="math-symbol math-null-symbol">'+t+"</span>":"undefined"===t?'<span class="math-symbol math-undefined-symbol">'+t+"</span>":'<span class="math-symbol">'+t+"</span>"},r.prototype.toJSON=function(){return{mathjs:"SymbolNode",name:this.name}},r.fromJSON=function(e){return new r(e.name)},r.prototype._toTex=function(e){var t=!1;void 0===n[this.name]&&s(this.name)&&(t=!0);var r=ff(this.name,t);return"\\"===r[0]?r:" "+r},r},{isClass:!0,isNode:!0});function qf(e){return(qf="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Bf(){return(Bf=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}var kf=["math","Node","SymbolNode"],zf=Object(s.a)("FunctionNode",kf,function(e){var i=e.math,t=e.Node,n=e.SymbolNode;function y(e,t){if(!(this instanceof y))throw new SyntaxError("Constructor must be called with the new operator");if("string"==typeof e&&(e=new n(e)),!Object(ie.w)(e))throw new TypeError('Node expected as parameter "fn"');if(!Array.isArray(t)||!t.every(ie.w))throw new TypeError('Array containing Nodes expected for parameter "args"');this.fn=e,this.args=t||[],Object.defineProperty(this,"name",{get:function(){return this.fn.name||""}.bind(this),set:function(){throw new Error("Cannot assign a new name, name is read-only")}});function r(){throw new Error("Property `FunctionNode.object` is deprecated, use `FunctionNode.fn` instead")}Object.defineProperty(this,"object",{get:r,set:r})}(y.prototype=new t).type="FunctionNode",y.prototype.isFunctionNode=!0,y.prototype._compile=function(i,t){if(!(this instanceof y))throw new TypeError("No valid FunctionNode");var a=Object(I.m)(this.args,function(e){return e._compile(i,t)});if(Object(ie.J)(this.fn)){var o=this.fn.name,s=o in i?Fi(i,o):void 0;if("function"==typeof s&&!0===s.rawArgs){var n=this.args;return function(e,t,r){return(o in e?Fi(e,o):s)(n,i,Bf({},e,t))}}if(1===a.length){var u=a[0];return function(e,t,r){return(o in e?Fi(e,o):s)(u(e,t,r))}}if(2!==a.length)return function(t,r,n){return(o in t?Fi(t,o):s).apply(null,Object(I.m)(a,function(e){return e(t,r,n)}))};var c=a[0],f=a[1];return function(e,t,r){return(o in e?Fi(e,o):s)(c(e,t,r),f(e,t,r))}}if(Object(ie.a)(this.fn)&&Object(ie.u)(this.fn.index)&&this.fn.index.isObjectProperty()){var l=this.fn.object._compile(i,t),p=this.fn.index.getObjectProperty(),m=this.args;return function(t,r,n){var e=l(t,r,n);return function(e,t){if(!Hi(e,t))throw new Error('No access to method "'+t+'"')}(e,p),e[p]&&e[p].rawArgs?e[p](m,i,Bf({},t,r)):e[p].apply(e,Object(I.m)(a,function(e){return e(t,r,n)}))}}var h=this.fn._compile(i,t),d=this.args;return function(t,r,n){var e=h(t,r,n);return e&&e.rawArgs?e(d,i,Bf({},t,r)):e.apply(e,Object(I.m)(a,function(e){return e(t,r,n)}))}},y.prototype.forEach=function(e){e(this.fn,"fn",this);for(var t=0;t<this.args.length;t++)e(this.args[t],"args["+t+"]",this)},y.prototype.map=function(e){for(var t=this._ifNode(e(this.fn,"fn",this)),r=[],n=0;n<this.args.length;n++)r[n]=this._ifNode(e(this.args[n],"args["+n+"]",this));return new y(t,r)},y.prototype.clone=function(){return new y(this.fn,this.args.slice(0))};var a=y.prototype.toString;function o(e,t,r){for(var n,i="",a=new RegExp("\\$(?:\\{([a-z_][a-z_0-9]*)(?:\\[([0-9]+)\\])?\\}|\\$)","ig"),o=0;null!==(n=a.exec(e));)if(i+=e.substring(o,n.index),o=n.index,"$$"===n[0])i+="$",o++;else{o+=n[0].length;var s=t[n[1]];if(!s)throw new ReferenceError("Template: Property "+n[1]+" does not exist.");if(void 0===n[2])switch(qf(s)){case"string":i+=s;break;case"object":if(Object(ie.w)(s))i+=s.toTex(r);else{if(!Array.isArray(s))throw new TypeError("Template: "+n[1]+" has to be a Node, String or array of Nodes");i+=s.map(function(e,t){if(Object(ie.w)(e))return e.toTex(r);throw new TypeError("Template: "+n[1]+"["+t+"] is not a Node.")}).join(",")}break;default:throw new TypeError("Template: "+n[1]+" has to be a Node, String or array of Nodes")}else{if(!Object(ie.w)(s[n[2]]&&s[n[2]]))throw new TypeError("Template: "+n[1]+"["+n[2]+"] is not a Node.");i+=s[n[2]].toTex(r)}}return i+=e.slice(o)}y.prototype.toString=function(e){var t,r=this.fn.toString(e);return e&&"object"===qf(e.handler)&&Object(ae.f)(e.handler,r)&&(t=e.handler[r](this,e)),void 0!==t?t:a.call(this,e)},y.prototype._toString=function(t){var e=this.args.map(function(e){return e.toString(t)});return(Object(ie.q)(this.fn)?"("+this.fn.toString(t)+")":this.fn.toString(t))+"("+e.join(", ")+")"},y.prototype.toJSON=function(){return{mathjs:"FunctionNode",fn:this.fn,args:this.args}},y.fromJSON=function(e){return new y(e.fn,e.args)},y.prototype.toHTML=function(t){var e=this.args.map(function(e){return e.toHTML(t)});return'<span class="math-function">'+Object(J.c)(this.fn)+'</span><span class="math-paranthesis math-round-parenthesis">(</span>'+e.join('<span class="math-separator">,</span>')+'<span class="math-paranthesis math-round-parenthesis">)</span>'};var r=y.prototype.toTex;return y.prototype.toTex=function(e){var t;return e&&"object"===qf(e.handler)&&Object(ae.f)(e.handler,this.name)&&(t=e.handler[this.name](this,e)),void 0!==t?t:r.call(this,e)},y.prototype._toTex=function(t){var e,r,n=this.args.map(function(e){return e.toTex(t)});switch(sf[this.name]&&(e=sf[this.name]),!i[this.name]||"function"!=typeof i[this.name].toTex&&"object"!==qf(i[this.name].toTex)&&"string"!=typeof i[this.name].toTex||(e=i[this.name].toTex),qf(e)){case"function":r=e(this,t);break;case"string":r=o(e,this,t);break;case"object":switch(qf(e[n.length])){case"function":r=e[n.length](this,t);break;case"string":r=o(e[n.length],this,t)}}return void 0!==r?r:o("\\mathrm{${name}}\\left(${args}\\right)",this,t)},y.prototype.getIdentifier=function(){return this.type+":"+this.name},y},{isClass:!0,isNode:!0});function Df(){return(Df=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}var Rf=["typed","numeric","config","AccessorNode","ArrayNode","AssignmentNode","BlockNode","ConditionalNode","ConstantNode","FunctionAssignmentNode","FunctionNode","IndexNode","ObjectNode","OperatorNode","ParenthesisNode","RangeNode","RelationalNode","SymbolNode"],Pf=Object(s.a)("parse",Rf,function(e){var t=e.typed,s=e.numeric,u=e.config,i=e.AccessorNode,c=e.ArrayNode,o=e.AssignmentNode,a=e.BlockNode,f=e.ConditionalNode,l=e.ConstantNode,p=e.FunctionAssignmentNode,m=e.FunctionNode,h=e.IndexNode,d=e.ObjectNode,y=e.OperatorNode,g=e.ParenthesisNode,n=e.RangeNode,v=e.RelationalNode,b=e.SymbolNode,x=t("parse",{string:function(e){return k(e,{})},"Array | Matrix":function(e){return r(e,{})},"string, Object":function(e,t){return k(e,void 0!==t.nodes?t.nodes:{})},"Array | Matrix, Object":r});function r(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=void 0!==t.nodes?t.nodes:{};return oe(e,function(e){if("string"!=typeof e)throw new TypeError("String expected");return k(e,r)})}var w={NULL:0,DELIMITER:1,NUMBER:2,SYMBOL:3,UNKNOWN:4},N={",":!0,"(":!0,")":!0,"[":!0,"]":!0,"{":!0,"}":!0,'"':!0,"'":!0,";":!0,"+":!0,"-":!0,"*":!0,".*":!0,"/":!0,"./":!0,"%":!0,"^":!0,".^":!0,"~":!0,"!":!0,"&":!0,"|":!0,"^|":!0,"=":!0,":":!0,"?":!0,"==":!0,"!=":!0,"<":!0,">":!0,"<=":!0,">=":!0,"<<":!0,">>":!0,">>>":!0},O={mod:!0,to:!0,in:!0,and:!0,xor:!0,or:!0,not:!0},M={true:!0,false:!1,null:null,undefined:void 0},E=["NaN","Infinity"];function j(e,t){return e.expression.substr(e.index,t)}function S(e){return j(e,1)}function A(e){e.index++}function C(e){return e.expression.charAt(e.index-1)}function T(e){return e.expression.charAt(e.index+1)}function _(e){for(e.tokenType=w.NULL,e.token="",e.comment="";x.isWhitespace(S(e),e.nestingLevel);)A(e);if("#"===S(e))for(;"\n"!==S(e)&&""!==S(e);)e.comment+=S(e),A(e);if(""!==S(e)){if("\n"===S(e)&&!e.nestingLevel)return e.tokenType=w.DELIMITER,e.token=S(e),void A(e);var t=S(e),r=j(e,2),n=j(e,3);if(3===n.length&&N[n])return e.tokenType=w.DELIMITER,e.token=n,A(e),A(e),void A(e);if(2===r.length&&N[r])return e.tokenType=w.DELIMITER,e.token=r,A(e),void A(e);if(N[t])return e.tokenType=w.DELIMITER,e.token=t,void A(e);if(x.isDigitDot(t)){if(e.tokenType=w.NUMBER,"."===S(e))e.token+=S(e),A(e),x.isDigit(S(e))||(e.tokenType=w.DELIMITER);else{for(;x.isDigit(S(e));)e.token+=S(e),A(e);x.isDecimalMark(S(e),T(e))&&(e.token+=S(e),A(e))}for(;x.isDigit(S(e));)e.token+=S(e),A(e);if("E"===S(e)||"e"===S(e))if(x.isDigit(T(e))||"-"===T(e)||"+"===T(e)){if(e.token+=S(e),A(e),"+"!==S(e)&&"-"!==S(e)||(e.token+=S(e),A(e)),!x.isDigit(S(e)))throw re(e,'Digit expected, got "'+S(e)+'"');for(;x.isDigit(S(e));)e.token+=S(e),A(e);if(x.isDecimalMark(S(e),T(e)))throw re(e,'Digit expected, got "'+S(e)+'"')}else if("."===T(e))throw A(e),re(e,'Digit expected, got "'+S(e)+'"')}else{if(!x.isAlpha(S(e),C(e),T(e))){for(e.tokenType=w.UNKNOWN;""!==S(e);)e.token+=S(e),A(e);throw re(e,'Syntax error in part "'+e.token+'"')}for(;x.isAlpha(S(e),C(e),T(e))||x.isDigit(S(e));)e.token+=S(e),A(e);Object(ae.f)(O,e.token)?e.tokenType=w.DELIMITER:e.tokenType=w.SYMBOL}}else e.tokenType=w.DELIMITER}function I(e){for(;_(e),"\n"===e.token;);}function q(e){e.nestingLevel++}function B(e){e.nestingLevel--}function k(e,t){var r={extraNodes:{},expression:"",comment:"",index:0,token:"",tokenType:w.NULL,nestingLevel:0,conditionalLevel:null};Df(r,{expression:e,extraNodes:t}),_(r);var n=function(e){var t,r,n=[];""!==e.token&&"\n"!==e.token&&";"!==e.token&&((t=z(e)).comment=e.comment);for(;"\n"===e.token||";"===e.token;)0===n.length&&t&&(r=";"!==e.token,n.push({node:t,visible:r})),_(e),"\n"!==e.token&&";"!==e.token&&""!==e.token&&((t=z(e)).comment=e.comment,r=";"!==e.token,n.push({node:t,visible:r}));return 0<n.length?new a(n):(t||((t=new l(void 0)).comment=e.comment),t)}(r);if(""!==r.token)throw r.tokenType===w.DELIMITER?ne(r,"Unexpected operator "+r.token):re(r,'Unexpected part "'+r.token+'"');return n}function z(e){var t,r,n,i,a=function(e){var t=function(e){var t=D(e);for(;"or"===e.token;)I(e),t=new y("or","or",[t,D(e)]);return t}(e);for(;"?"===e.token;){var r=e.conditionalLevel;e.conditionalLevel=e.nestingLevel,I(e);var n=t,i=z(e);if(":"!==e.token)throw re(e,"False part of conditional expression expected");e.conditionalLevel=null,I(e);var a=z(e);t=new f(n,i,a),e.conditionalLevel=r}return t}(e);if("="!==e.token)return a;if(Object(ie.J)(a))return t=a.name,I(e),n=z(e),new o(new b(t),n);if(Object(ie.a)(a))return I(e),n=z(e),new o(a.object,a.index,n);if(Object(ie.r)(a)&&Object(ie.J)(a.fn)&&(i=!0,r=[],t=a.name,a.args.forEach(function(e,t){Object(ie.J)(e)?r[t]=e.name:i=!1}),i))return I(e),n=z(e),new p(t,r,n);throw re(e,"Invalid left hand side of assignment operator =")}function D(e){for(var t=R(e);"xor"===e.token;)I(e),t=new y("xor","xor",[t,R(e)]);return t}function R(e){for(var t=P(e);"and"===e.token;)I(e),t=new y("and","and",[t,P(e)]);return t}function P(e){for(var t=F(e);"|"===e.token;)I(e),t=new y("|","bitOr",[t,F(e)]);return t}function F(e){for(var t=U(e);"^|"===e.token;)I(e),t=new y("^|","bitXor",[t,U(e)]);return t}function U(e){for(var t=L(e);"&"===e.token;)I(e),t=new y("&","bitAnd",[t,L(e)]);return t}function L(e){for(var t=[H(e)],r=[],n={"==":"equal","!=":"unequal","<":"smaller",">":"larger","<=":"smallerEq",">=":"largerEq"};Object(ae.f)(n,e.token);){var i={name:e.token,fn:n[e.token]};r.push(i),I(e),t.push(H(e))}return 1===t.length?t[0]:2===t.length?new y(r[0].name,r[0].fn,t):new v(r.map(function(e){return e.fn}),t)}function H(e){var t,r,n,i;t=$(e);for(var a={"<<":"leftShift",">>":"rightArithShift",">>>":"rightLogShift"};Object(ae.f)(a,e.token);)n=a[r=e.token],I(e),i=[t,$(e)],t=new y(r,n,i);return t}function $(e){var t,r,n,i;t=G(e);for(var a={to:"to",in:"to"};Object(ae.f)(a,e.token);)n=a[r=e.token],I(e),t="in"===r&&""===e.token?new y("*","multiply",[t,new b("in")],!0):(i=[t,G(e)],new y(r,n,i));return t}function G(e){var t,r=[];if(t=":"===e.token?new l(1):Z(e),":"===e.token&&e.conditionalLevel!==e.nestingLevel){for(r.push(t);":"===e.token&&r.length<3;)I(e),")"===e.token||"]"===e.token||","===e.token||""===e.token?r.push(new b("end")):r.push(Z(e));t=3===r.length?new n(r[0],r[2],r[1]):new n(r[0],r[1])}return t}function Z(e){var t,r,n,i;t=V(e);for(var a={"+":"add","-":"subtract"};Object(ae.f)(a,e.token);)n=a[r=e.token],I(e),i=[t,V(e)],t=new y(r,n,i);return t}function V(e){var t,r,n,i;r=t=J(e);for(var a={"*":"multiply",".*":"dotMultiply","/":"divide","./":"dotDivide","%":"mod",mod:"mod"};Object(ae.f)(a,e.token);)i=a[n=e.token],I(e),r=J(e),t=new y(n,i,[t,r]);return t}function J(e){var t,r;for(r=t=W(e);e.tokenType===w.SYMBOL||"in"===e.token&&Object(ie.l)(t)||!(e.tokenType!==w.NUMBER||Object(ie.l)(r)||Object(ie.B)(r)&&"!"!==r.op)||"("===e.token;)r=W(e),t=new y("*","multiply",[t,r],!0);return t}function W(e){for(var t=Y(e),r=t,n=[];"/"===e.token&&Object(ie.l)(r);){if(n.push(Df({},e)),I(e),e.tokenType!==w.NUMBER){Df(e,n.pop());break}if(n.push(Df({},e)),I(e),e.tokenType!==w.SYMBOL&&"("!==e.token){n.pop(),Df(e,n.pop());break}Df(e,n.pop()),n.pop(),r=Y(e),t=new y("/","divide",[t,r])}return t}function Y(e){var t,r,n,i={"-":"unaryMinus","+":"unaryPlus","~":"bitNot",not:"not"};return Object(ae.f)(i,e.token)?(n=i[e.token],t=e.token,I(e),r=[Y(e)],new y(t,n,r)):function(e){var t,r,n,i;t=function(e){var t,r,n;t=function(e){var t=[];if(e.tokenType===w.SYMBOL&&Object(ae.f)(e.extraNodes,e.token)){var r=e.extraNodes[e.token];if(_(e),"("===e.token){if(t=[],q(e),_(e),")"!==e.token)for(t.push(z(e));","===e.token;)_(e),t.push(z(e));if(")"!==e.token)throw re(e,"Parenthesis ) expected");B(e),_(e)}return new r(t)}return function(e){var t,r;if(e.tokenType===w.SYMBOL||e.tokenType===w.DELIMITER&&e.token in O)return r=e.token,_(e),t=Object(ae.f)(M,r)?new l(M[r]):-1!==E.indexOf(r)?new l(s(r,"number")):new b(r),t=X(e,t);return function(e){var t,r;return'"'!==e.token?function(e){var t,r;return"'"!==e.token?function(e){var t,r,n,i;if("["!==e.token)return function(e){if("{"!==e.token)return function(e){var t;return e.tokenType!==w.NUMBER?function(e){var t;if("("!==e.token)return function(e){throw""===e.token?re(e,"Unexpected end of expression"):re(e,"Value expected")}(e);if(q(e),_(e),t=z(e),")"===e.token)return B(e),_(e),t=new g(t),t=X(e,t);throw re(e,"Parenthesis ) expected")}(e):(t=e.token,_(e),new l(s(t,u.number)))}(e);var t;q(e);var r={};do{if(_(e),"}"!==e.token){if('"'===e.token)t=Q(e);else if("'"===e.token)t=K(e);else{if(e.tokenType!==w.SYMBOL)throw re(e,"Symbol or string expected as object key");t=e.token,_(e)}if(":"!==e.token)throw re(e,"Colon : expected after object key");_(e),r[t]=z(e)}}while(","===e.token);if("}"!==e.token)throw re(e,"Comma , or bracket } expected after object value");B(e),_(e);var n=new d(r);return n=X(e,n)}(e);if(q(e),_(e),"]"!==e.token){var a=ee(e);if(";"===e.token){for(n=1,r=[a];";"===e.token;)_(e),r[n]=ee(e),n++;if("]"!==e.token)throw re(e,"End of matrix ] expected");B(e),_(e),i=r[0].items.length;for(var o=1;o<n;o++)if(r[o].items.length!==i)throw ne(e,"Column dimensions mismatch ("+r[o].items.length+" !== "+i+")");t=new c(r)}else{if("]"!==e.token)throw re(e,"End of matrix ] expected");B(e),_(e),t=a}}else B(e),_(e),t=new c([]);return X(e,t)}(e):(r=K(e),t=new l(r),t=X(e,t))}(e):(r=Q(e),t=new l(r),t=X(e,t))}(e)}(e)}(e);var i={"!":"factorial","'":"ctranspose"};for(;Object(ae.f)(i,e.token);)r=e.token,n=i[r],_(e),t=new y(r,n,[t]),t=X(e,t);return t}(e),"^"!==e.token&&".^"!==e.token||(r=e.token,n="^"===r?"pow":"dotPow",I(e),i=[t,Y(e)],t=new y(r,n,i));return t}(e)}function X(e,t,r){for(var n;!("("!==e.token&&"["!==e.token&&"."!==e.token||r&&-1===r.indexOf(e.token));)if(n=[],"("===e.token){if(!Object(ie.J)(t)&&!Object(ie.a)(t))return t;if(q(e),_(e),")"!==e.token)for(n.push(z(e));","===e.token;)_(e),n.push(z(e));if(")"!==e.token)throw re(e,"Parenthesis ) expected");B(e),_(e),t=new m(t,n)}else if("["===e.token){if(q(e),_(e),"]"!==e.token)for(n.push(z(e));","===e.token;)_(e),n.push(z(e));if("]"!==e.token)throw re(e,"Parenthesis ] expected");B(e),_(e),t=new i(t,new h(n))}else{if(_(e),e.tokenType!==w.SYMBOL)throw re(e,"Property name expected after dot");n.push(new l(e.token)),_(e);t=new i(t,new h(n,!0))}return t}function Q(e){for(var t="";""!==S(e)&&'"'!==S(e);)"\\"===S(e)&&(t+=S(e),A(e)),t+=S(e),A(e);if(_(e),'"'!==e.token)throw re(e,'End of string " expected');return _(e),JSON.parse('"'+t+'"')}function K(e){for(var t="";""!==S(e)&&"'"!==S(e);)"\\"===S(e)&&(t+=S(e),A(e)),t+=S(e),A(e);if(_(e),"'"!==e.token)throw re(e,"End of string ' expected");return _(e),JSON.parse('"'+t+'"')}function ee(e){for(var t=[z(e)],r=1;","===e.token;)_(e),t[r]=z(e),r++;return new c(t)}function te(e){return e.index-e.token.length+1}function re(e,t){var r=te(e),n=new SyntaxError(t+" (char "+r+")");return n.char=r,n}function ne(e,t){var r=te(e),n=new SyntaxError(t+" (char "+r+")");return n.char=r,n}return x.isAlpha=function(e,t,r){return x.isValidLatinOrGreek(e)||x.isValidMathSymbol(e,r)||x.isValidMathSymbol(t,e)},x.isValidLatinOrGreek=function(e){return/^[a-zA-Z_$\u00C0-\u02AF\u0370-\u03FF\u2100-\u214F]$/.test(e)},x.isValidMathSymbol=function(e,t){return/^[\uD835]$/.test(e)&&/^[\uDC00-\uDFFF]$/.test(t)&&/^[^\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]$/.test(t)},x.isWhitespace=function(e,t){return" "===e||"\t"===e||"\n"===e&&0<t},x.isDecimalMark=function(e,t){return"."===e&&"/"!==t&&"*"!==t&&"^"!==t},x.isDigitDot=function(e){return"0"<=e&&e<="9"||"."===e},x.isDigit=function(e){return"0"<=e&&e<="9"},x}),Ff="compile",Uf=["typed","parse"],Lf=Object(s.a)(Ff,Uf,function(e){var t=e.typed,r=e.parse;return t(Ff,{string:function(e){return r(e).compile()},"Array | Matrix":function(e){return oe(e,function(e){return r(e).compile()})}})}),Hf="evaluate",$f=["typed","parse"],Gf=Object(s.a)(Hf,$f,function(e){var t=e.typed,r=e.parse;return t(Hf,{string:function(e){return r(e).compile().evaluate({})},"string, Object":function(e,t){return r(e).compile().evaluate(t)},"Array | Matrix":function(e){var t={};return oe(e,function(e){return r(e).compile().evaluate(t)})},"Array | Matrix, Object":function(e,t){return oe(e,function(e){return r(e).compile().evaluate(t)})}})}),Zf=Object(s.a)("eval",["evaluate"],function(e){var n=e.evaluate;return function(){Object(ve.a)('Function "eval" has been renamed to "evaluate" in v6.0.0, please use the new function instead.');for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return n.apply(n,t)}}),Vf=["parse"],Jf=Object(s.a)("Parser",Vf,function(e){var t=e.parse;function r(){if(!(this instanceof r))throw new SyntaxError("Constructor must be called with the new operator");this.scope={}}return r.prototype.type="Parser",r.prototype.isParser=!0,r.prototype.parse=function(e){throw new Error("Parser.parse is deprecated. Use math.parse instead.")},r.prototype.compile=function(e){throw new Error("Parser.compile is deprecated. Use math.compile instead.")},r.prototype.evaluate=function(e){return t(e).compile().evaluate(this.scope)},r.prototype.eval=function(e){return Object(ve.a)("Method Parser.eval is renamed to Parser.evaluate. Please use the new method name."),this.evaluate(e)},r.prototype.get=function(e){return e in this.scope?Fi(this.scope,e):void 0},r.prototype.getAll=function(){return Object(ae.e)({},this.scope)},r.prototype.set=function(e,t){return Ui(this.scope,e,t)},r.prototype.remove=function(e){delete this.scope[e]},r.prototype.clear=function(){for(var e in this.scope)Object(ae.f)(this.scope,e)&&delete this.scope[e]},r},{isClass:!0}),Wf=["typed","Parser"],Yf=Object(s.a)("parser",Wf,function(e){var t=e.typed,r=e.Parser;return t("parser",{"":function(){return new r}})}),Xf=["typed","matrix","abs","addScalar","divideScalar","multiplyScalar","subtract","larger","equalScalar","unaryMinus","DenseMatrix","SparseMatrix","Spa"],Qf=Object(s.a)("lup",Xf,function(e){var t=e.typed,r=e.matrix,M=e.abs,E=e.addScalar,j=e.divideScalar,S=e.multiplyScalar,A=e.subtract,C=e.larger,T=e.equalScalar,O=e.unaryMinus,_=e.DenseMatrix,I=e.SparseMatrix,q=e.Spa;return t("lup",{DenseMatrix:function(e){return n(e)},SparseMatrix:function(e){return function(e){var r,s,u,c=e._size[0],t=e._size[1],n=Math.min(c,t),f=e._values,l=e._index,p=e._ptr,m=[],h=[],d=[],y=[c,n],g=[],v=[],b=[],x=[n,t],w=[],N=[];for(r=0;r<c;r++)w[r]=r,N[r]=r;function i(){var i=new q;s<c&&(d.push(m.length),m.push(1),h.push(s)),b.push(g.length);var e=p[s],t=p[s+1];for(u=e;u<t;u++)r=l[u],i.set(w[r],f[u]);0<s&&i.forEach(0,s-1,function(r,n){I._forEachRow(r,m,h,d,function(e,t){r<e&&i.accumulate(e,O(S(t,n)))})});var n=s,a=i.get(s),o=M(a);i.forEach(s+1,c-1,function(e,t){var r=M(t);C(r,o)&&(n=e,o=r,a=t)}),s!==n&&(I._swapRows(s,n,y[1],m,h,d),I._swapRows(s,n,x[1],g,v,b),i.swap(s,n),function(e,t){var r=N[e],n=N[t];w[r]=t,w[n]=e,N[e]=n,N[t]=r}(s,n)),i.forEach(0,c-1,function(e,t){e<=s?(g.push(t),v.push(e)):(t=j(t,a),T(t,0)||(m.push(t),h.push(e)))})}for(s=0;s<t;s++)i();return b.push(g.length),d.push(m.length),{L:new I({values:m,index:h,ptr:d,size:y}),U:new I({values:g,index:v,ptr:b,size:x}),p:w,toString:function(){return"L: "+this.L.toString()+"\nU: "+this.U.toString()+"\nP: "+this.p}}}(e)},Array:function(e){var t=n(r(e));return{L:t.L.valueOf(),U:t.U.valueOf(),p:t.p}}});function n(e){var t,r,n,i=e._size[0],a=e._size[1],o=Math.min(i,a),s=Object(ae.a)(e._data),u=[],c=[i,o],f=[],l=[o,a],p=[];for(t=0;t<i;t++)p[t]=t;for(r=0;r<a;r++){if(0<r)for(t=0;t<i;t++){var m=Math.min(t,r),h=0;for(n=0;n<m;n++)h=E(h,S(s[t][n],s[n][r]));s[t][r]=A(s[t][r],h)}var d=r,y=0,g=0;for(t=r;t<i;t++){var v=s[t][r],b=M(v);C(b,y)&&(d=t,y=b,g=v)}if(r!==d&&(p[r]=[p[d],p[d]=p[r]][0],_._swapRows(r,d,s)),r<i)for(t=r+1;t<i;t++){var x=s[t][r];T(x,0)||(s[t][r]=j(s[t][r],g))}}for(r=0;r<a;r++)for(t=0;t<i;t++)0===r&&(t<a&&(f[t]=[]),u[t]=[]),t<r?(t<a&&(f[t][r]=s[t][r]),r<i&&(u[t][r]=0)):t!==r?(t<a&&(f[t][r]=0),r<i&&(u[t][r]=s[t][r])):(t<a&&(f[t][r]=s[t][r]),r<i&&(u[t][r]=1));var w=new _({data:u,size:c}),N=new _({data:f,size:l}),O=[];for(t=0,o=p.length;t<o;t++)O[p[t]]=t;return{L:w,U:N,p:O,toString:function(){return"L: "+this.L.toString()+"\nU: "+this.U.toString()+"\nP: "+this.p}}}}),Kf=["typed","matrix","zeros","identity","isZero","unequal","sign","sqrt","conj","unaryMinus","addScalar","divideScalar","multiplyScalar","subtract"],el=Object(s.a)("qr",Kf,function(e){var t=e.typed,r=e.matrix,b=e.zeros,x=e.identity,w=e.isZero,N=e.unequal,O=e.sign,M=e.sqrt,E=e.conj,j=e.unaryMinus,S=e.addScalar,A=e.divideScalar,C=e.multiplyScalar,T=e.subtract;return t("qr",{DenseMatrix:function(e){return n(e)},SparseMatrix:function(e){return function(){throw new Error("qr not implemented for sparse matrices yet")}()},Array:function(e){var t=n(r(e));return{Q:t.Q.valueOf(),R:t.R.valueOf()}}});function n(e){var t,r,n,i=e._size[0],a=e._size[1],o=x([i],"dense"),s=o._data,u=e.clone(),c=u._data,f=b([i],"");for(n=0;n<Math.min(a,i);++n){var l=c[n][n],p=j(O(l)),m=E(p),h=0;for(t=n;t<i;t++)h=S(h,C(c[t][n],E(c[t][n])));var d=C(p,M(h));if(!w(d)){var y=T(l,d);for(t=n+(f[n]=1);t<i;t++)f[t]=A(c[t][n],y);var g=j(E(A(y,d))),v=void 0;for(r=n;r<a;r++){for(v=0,t=n;t<i;t++)v=S(v,C(E(f[t]),c[t][r]));for(v=C(v,g),t=n;t<i;t++)c[t][r]=C(T(c[t][r],C(f[t],v)),m)}for(t=0;t<i;t++){for(v=0,r=n;r<i;r++)v=S(v,C(s[t][r],f[r]));for(v=C(v,g),r=n;r<i;++r)s[t][r]=A(T(s[t][r],C(v,E(f[r]))),m)}}}for(t=0;t<i;++t)for(r=0;r<t&&r<a;++r){if(N(0,A(c[t][r],1e5)))throw new Error("math.qr(): unknown error - R is not lower triangular (element ("+t+", "+r+")  = "+c[t][r]+")");c[t][r]=C(c[t][r],0)}return{Q:o,R:u,toString:function(){return"Q: "+this.Q.toString()+"\nR: "+this.R.toString()}}}});function tl(e,t,r,n,i,a,o){var s=0;for(r[o]=e;0<=s;){var u=r[o+s],c=r[n+u];-1===c?(s--,a[t++]=u):(r[n+u]=r[i+c],r[o+ ++s]=c)}return t}function rl(e){return-e-2}var nl=["add","multiply","transpose"],il=Object(s.a)("csAmd",nl,function(e){var K=e.add,ee=e.multiply,te=e.transpose;return function(e,t){if(!t||e<=0||3<e)return null;var r=t._size,n=r[0],i=r[1],a=0,o=Math.max(16,10*Math.sqrt(i)),s=function(e,t,r,n,i){var a=te(t);if(1===e&&n===r)return K(t,a);if(2!==e)return ee(a,t);for(var o=a._index,s=a._ptr,u=0,c=0;c<r;c++){var f=s[c];if(s[c]=u,!(s[c+1]-f>i))for(var l=s[c+1];f<l;f++)o[u++]=o[f]}return s[r]=u,t=te(a),ee(a,t)}(e,t,n,i,o=Math.min(i-2,o));!function(e,t,r){for(var n=e._values,i=e._index,a=e._ptr,o=e._size[1],s=0,u=0;u<o;u++){var c=a[u];for(a[u]=s;c<a[u+1];c++)t(i[c],u,n?n[c]:1,r)&&(i[s]=i[c],n&&(n[s]=n[c]),s++)}a[o]=s,i.splice(s,i.length-s),n&&n.splice(s,n.length-s)}(s,ne,null);for(var u,c,f,l,p,m,h,d,y,g,v,b,x,w,N,O,M=s._index,E=s._ptr,j=E[i],S=[],A=[],C=i+1,T=2*(i+1),_=3*(i+1),I=4*(i+1),q=5*(i+1),B=6*(i+1),k=7*(i+1),z=S,D=function(e,t,r,n,i,a,o,s,u,c,f,l){for(var p=0;p<e;p++)r[n+p]=t[p+1]-t[p];for(var m=r[n+e]=0;m<=e;m++)r[i+m]=-1,a[m]=-1,r[o+m]=-1,r[s+m]=-1,r[u+m]=1,r[c+m]=1,r[f+m]=0,r[l+m]=r[n+m];var h=re(0,0,r,c,e);return r[f+e]=-2,t[e]=-1,r[c+e]=0,h}(i,E,A,0,_,z,T,k,C,B,I,q),R=function(e,t,r,n,i,a,o,s,u,c,f){for(var l=0,p=0;p<e;p++){var m=r[n+p];if(0===m)r[i+p]=-2,l++,t[p]=-1,r[a+p]=0;else if(o<m)r[s+p]=0,r[i+p]=-1,l++,t[p]=rl(e),r[s+e]++;else{var h=r[u+m];-1!==h&&(c[h]=p),r[f+p]=r[u+m],r[u+m]=p}}return l}(i,E,A,q,I,B,o,C,_,z,T),P=0;R<i;){for(f=-1;P<i&&-1===(f=A[_+P]);P++);-1!==A[T+f]&&(z[A[T+f]]=-1),A[_+P]=A[T+f];var F=A[I+f],U=A[C+f];R+=U;var L=0;A[C+f]=-U;var H=E[f],$=0===F?H:j,G=$;for(l=1;l<=F+1;l++){for(d=F<l?(h=H,A[0+(m=f)]-F):(h=E[m=M[H++]],A[0+m]),p=1;p<=d;p++)(y=A[C+(u=M[h++])])<=0||(L+=y,A[C+u]=-y,-1!==A[T+(M[G++]=u)]&&(z[A[T+u]]=z[u]),-1!==z[u]?A[T+z[u]]=A[T+u]:A[_+A[q+u]]=A[T+u]);m!==f&&(E[m]=rl(f),A[B+m]=0)}for(0!==F&&(j=G),A[q+f]=L,E[f]=$,A[0+f]=G-$,A[I+f]=-2,D=re(D,a,A,B,i),g=$;g<G;g++)if(!((v=A[I+(u=M[g])])<=0)){var Z=D-(y=-A[C+u]);for(H=E[u],b=E[u]+v-1;H<=b;H++)A[B+(m=M[H])]>=D?A[B+m]-=y:0!==A[B+m]&&(A[B+m]=A[q+m]+Z)}for(g=$;g<G;g++){for(x=(b=E[u=M[g]])+A[I+u]-1,O=N=0,H=w=b;H<=x;H++)if(0!==A[B+(m=M[H])]){var V=A[B+m]-D;0<V?(O+=V,N+=M[w++]=m):(E[m]=rl(f),A[B+m]=0)}A[I+u]=w-b+1;var J=w,W=b+A[0+u];for(H=1+x;H<W;H++){var Y=A[C+(c=M[H])];Y<=0||(O+=Y,N+=M[w++]=c)}0===O?(E[u]=rl(f),L-=y=-A[C+u],U+=y,R+=y,A[C+u]=0,A[I+u]=-1):(A[q+u]=Math.min(A[q+u],O),M[w]=M[J],M[J]=M[b],M[b]=f,A[0+u]=w-b+1,N=(N<0?-N:N)%i,A[T+u]=A[k+N],z[A[k+N]=u]=N)}for(A[q+f]=L,D=re(D+(a=Math.max(a,L)),a,A,B,i),g=$;g<G;g++)if(!(0<=A[C+(u=M[g])]))for(u=A[k+(N=z[u])],A[k+N]=-1;-1!==u&&-1!==A[T+u];u=A[T+u],D++){for(d=A[0+u],v=A[I+u],H=E[u]+1;H<=E[u]+d-1;H++)A[B+M[H]]=D;var X=u;for(c=A[T+u];-1!==c;){var Q=A[0+c]===d&&A[I+c]===v;for(H=E[c]+1;Q&&H<=E[c]+d-1;H++)A[B+M[H]]!==D&&(Q=0);Q?(E[c]=rl(u),A[C+u]+=A[C+c],A[C+c]=0,A[I+c]=-1,c=A[T+c],A[T+X]=c):c=A[T+(X=c)]}}for(g=H=$;g<G;g++)(y=-A[C+(u=M[g])])<=0||(A[C+u]=y,O=A[q+u]+L-y,-1!==A[_+(O=Math.min(O,i-R-y))]&&(z[A[_+O]]=u),A[T+u]=A[_+O],z[u]=-1,A[_+O]=u,P=Math.min(P,O),A[q+u]=O,M[H++]=u);A[C+f]=U,0==(A[0+f]=H-$)&&(E[f]=-1,A[B+f]=0),0!==F&&(j=H)}for(u=0;u<i;u++)E[u]=rl(E[u]);for(c=0;c<=i;c++)A[_+c]=-1;for(c=i;0<=c;c--)0<A[C+c]||(A[T+c]=A[_+E[c]],A[_+E[c]]=c);for(m=i;0<=m;m--)A[C+m]<=0||-1!==E[m]&&(A[T+m]=A[_+E[m]],A[_+E[m]]=m);for(u=f=0;u<=i;u++)-1===E[u]&&(f=tl(u,f,A,_,T,S,B));return S.splice(S.length-1,1),S};function re(e,t,r,n,i){if(e<2||e+t<0){for(var a=0;a<i;a++)0!==r[n+a]&&(r[n+a]=1);e=2}return e}function ne(e,t){return e!==t}});function al(e,t,r,n,i,a,o){var s,u,c,f=0;if(e<=t||r[n+t]<=r[i+e])return-1;r[i+e]=r[n+t];var l=r[a+e];if(r[a+e]=t,-1===l)f=1,c=e;else{for(f=2,c=l;c!==r[o+c];c=r[o+c]);for(s=l;s!==c;s=u)u=r[o+s],r[o+s]=c}return{jleaf:f,q:c}}var ol=["transpose"],sl=Object(s.a)("csCounts",ol,function(e){var j=e.transpose;return function(e,t,r,n){if(!e||!t||!r)return null;var i,a,o,s,u,c,f,l=e._size,p=l[0],m=l[1],h=4*m+(n?m+p+1:0),d=[],y=m,g=2*m,v=3*m,b=4*m,x=5*m+1;for(o=0;o<h;o++)d[o]=-1;var w=[],N=j(e),O=N._index,M=N._ptr;for(o=0;o<m;o++)for(w[a=r[o]]=-1===d[v+a]?1:0;-1!==a&&-1===d[v+a];a=t[a])d[v+a]=o;if(n){for(o=0;o<m;o++)d[r[o]]=o;for(i=0;i<p;i++){for(o=m,c=M[i],f=M[i+1],u=c;u<f;u++)o=Math.min(o,d[O[u]]);d[x+i]=d[b+o],d[b+o]=i}}for(i=0;i<m;i++)d[0+i]=i;for(o=0;o<m;o++){for(-1!==t[a=r[o]]&&w[t[a]]--,s=n?d[b+o]:a;-1!==s;s=n?d[x+s]:-1)for(u=M[s];u<M[s+1];u++){var E=al(i=O[u],a,d,v,y,g,0);1<=E.jleaf&&w[a]++,2===E.jleaf&&w[E.q]--}-1!==t[a]&&(d[0+a]=t[a])}for(a=0;a<m;a++)-1!==t[a]&&(w[t[a]]+=w[a]);return w}}),ul=["add","multiply","transpose"],cl=Object(s.a)("csSqr",ul,function(e){var t=e.add,r=e.multiply,n=e.transpose,c=il({add:t,multiply:r,transpose:n}),f=sl({transpose:n});return function(e,t,r){var n,i=t._ptr,a=t._size[1],o={};if(o.q=c(e,t),e&&!o.q)return null;if(r){var s=e?function(e,t,r,n){for(var i=e._values,a=e._index,o=e._ptr,s=e._size,u=e._datatype,c=s[0],f=s[1],l=n&&e._values?[]:null,p=[],m=[],h=0,d=0;d<f;d++){m[d]=h;for(var y=r?r[d]:d,g=o[y],v=o[y+1],b=g;b<v;b++){var x=t?t[a[b]]:a[b];p[h]=x,l&&(l[h]=i[b]),h++}}return m[f]=h,e.createSparseMatrix({values:l,index:p,ptr:m,size:[c,f],datatype:u})}(t,null,o.q,0):t;o.parent=function(e,t){if(!e)return null;var r,n,i=e._index,a=e._ptr,o=e._size,s=o[0],u=o[1],c=[],f=[],l=u;if(t)for(r=0;r<s;r++)f[l+r]=-1;for(var p=0;p<u;p++){c[p]=-1,f[0+p]=-1;for(var m=a[p],h=a[p+1],d=m;d<h;d++){var y=i[d];for(r=t?f[l+y]:y;-1!==r&&r<p;r=n)n=f[0+r],f[0+r]=p,-1===n&&(c[r]=p);t&&(f[l+y]=p)}}return c}(s,1);var u=function(e,t){if(!e)return null;var r,n=0,i=[],a=[],o=t,s=2*t;for(r=0;r<t;r++)a[0+r]=-1;for(r=t-1;0<=r;r--)-1!==e[r]&&(a[o+r]=a[0+e[r]],a[0+e[r]]=r);for(r=0;r<t;r++)-1===e[r]&&(n=tl(r,n,a,0,o,i,s));return i}(o.parent,a);if(o.cp=f(s,o.parent,u,1),s&&o.parent&&o.cp&&function(e,t){var r=e._ptr,n=e._index,i=e._size,a=i[0],o=i[1];t.pinv=[],t.leftmost=[];var s,u,c,f,l,p=t.parent,m=t.pinv,h=t.leftmost,d=[],y=a,g=a+o,v=a+2*o;for(u=0;u<o;u++)d[y+u]=-1,d[g+u]=-1,d[v+u]=0;for(s=0;s<a;s++)h[s]=-1;for(u=o-1;0<=u;u--)for(f=r[u],l=r[u+1],c=f;c<l;c++)h[n[c]]=u;for(s=a-1;0<=s;s--)(m[s]=-1)!==(u=h[s])&&(0==d[v+u]++&&(d[g+u]=s),d[0+s]=d[y+u],d[y+u]=s);for(t.lnz=0,t.m2=a,u=0;u<o;u++)if(s=d[y+u],t.lnz++,s<0&&(s=t.m2++),m[s]=u,!(--v[u]<=0)){t.lnz+=d[v+u];var b=p[u];-1!==b&&(0===d[v+b]&&(d[g+b]=d[g+u]),d[0+d[g+u]]=d[y+b],d[y+b]=d[0+s],d[v+b]+=d[v+u])}for(s=0;s<a;s++)m[s]<0&&(m[s]=u++);return!0}(s,o))for(n=o.unz=0;n<a;n++)o.unz+=o.cp[n]}else o.unz=4*i[a]+a,o.lnz=o.unz;return o}});function fl(e,t){return e[t]<0}function ll(e,t){e[t]=rl(e[t])}function pl(e){return e<0?rl(e):e}function ml(e,t,r,n,i){var a,o,s,u=t._index,c=t._ptr,f=t._size[1],l=0;for(n[0]=e;0<=l;){e=n[l];var p=i?i[e]:e;fl(c,e)||(ll(c,e),n[f+l]=p<0?0:pl(c[p]));var m=1;for(o=n[f+l],s=p<0?0:pl(c[p+1]);o<s;o++)if(!fl(c,a=u[o])){n[f+l]=o,n[++l]=a,m=0;break}m&&(l--,n[--r]=e)}return r}var hl=["divideScalar","multiply","subtract"],dl=Object(s.a)("csSpsolve",hl,function(e){var O=e.divideScalar,M=e.multiply,E=e.subtract;return function(e,t,r,n,i,a,o){var s,u,c,f,l=e._values,p=e._index,m=e._ptr,h=e._size[1],d=t._values,y=t._index,g=t._ptr,v=function(e,t,r,n,i){var a,o,s,u=e._ptr,c=e._size,f=t._index,l=t._ptr,p=c[1],m=p;for(o=l[r],s=l[r+1],a=o;a<s;a++){var h=f[a];fl(u,h)||(m=ml(h,e,m,n,i))}for(a=m;a<p;a++)ll(u,n[a]);return m}(e,t,r,n,a);for(s=v;s<h;s++)i[n[s]]=0;for(u=g[r],c=g[r+1],s=u;s<c;s++)i[y[s]]=d[s];for(var b=v;b<h;b++){var x=n[b],w=a?a[x]:x;if(!(w<0))for(u=m[w],c=m[w+1],i[x]=O(i[x],l[o?u:c-1]),s=o?u+1:u,f=o?c:c-1;s<f;s++){var N=p[s];i[N]=E(i[N],M(l[s],i[x]))}}return v}}),yl=["abs","divideScalar","multiply","subtract","larger","largerEq","SparseMatrix"],gl=Object(s.a)("csLu",yl,function(e){var S=e.abs,A=e.divideScalar,C=e.multiply,t=e.subtract,T=e.larger,_=e.largerEq,I=e.SparseMatrix,q=dl({divideScalar:A,multiply:C,subtract:t});return function(e,t,r){if(!e)return null;var n,i=e._size[1],a=100,o=100;t&&(n=t.q,a=t.lnz||a,o=t.unz||o);var s,u,c=[],f=[],l=[],p=new I({values:c,index:f,ptr:l,size:[i,i]}),m=[],h=[],d=[],y=new I({values:m,index:h,ptr:d,size:[i,i]}),g=[],v=[],b=[];for(s=0;s<i;s++)v[s]=0,g[s]=-1,l[s+1]=0;for(var x=o=a=0;x<i;x++){l[x]=a,d[x]=o;var w=n?n[x]:x,N=q(p,e,w,b,v,g,1),O=-1,M=-1;for(u=N;u<i;u++)if(g[s=b[u]]<0){var E=S(v[s]);T(E,M)&&(M=E,O=s)}else h[o]=g[s],m[o++]=v[s];if(-1===O||M<=0)return null;g[w]<0&&_(S(v[w]),C(M,r))&&(O=w);var j=v[O];for(h[o]=x,m[o++]=j,g[O]=x,f[a]=O,c[a++]=1,u=N;u<i;u++)g[s=b[u]]<0&&(f[a]=s,c[a++]=A(v[s],j)),v[s]=0}for(l[i]=a,d[i]=o,u=0;u<a;u++)f[u]=g[f[u]];return c.splice(a,c.length-a),f.splice(a,f.length-a),m.splice(o,m.length-o),h.splice(o,h.length-o),{L:p,U:y,pinv:g}}}),vl=["typed","abs","add","multiply","transpose","divideScalar","subtract","larger","largerEq","SparseMatrix"],bl=Object(s.a)("slu",vl,function(e){var t=e.typed,r=e.abs,n=e.add,i=e.multiply,a=e.transpose,o=e.divideScalar,s=e.subtract,u=e.larger,c=e.largerEq,f=e.SparseMatrix,l=cl({add:n,multiply:i,transpose:a}),p=gl({abs:r,divideScalar:o,multiply:i,subtract:s,larger:u,largerEq:c,SparseMatrix:f});return t("slu",{"SparseMatrix, number, number":function(e,t,r){if(!Object(j.i)(t)||t<0||3<t)throw new Error("Symbolic Ordering and Analysis order must be an integer number in the interval [0, 3]");if(r<0||1<r)throw new Error("Partial pivoting threshold must be a number from 0 to 1");var n=l(t,e,!1),i=p(e,n,r);return{L:i.L,U:i.U,p:i.pinv,q:n.q,toString:function(){return"L: "+this.L.toString()+"\nU: "+this.U.toString()+"\np: "+this.p.toString()+(this.q?"\nq: "+this.q.toString():"")+"\n"}}}})});function xl(e,t){var r,n=t.length,i=[];if(e)for(r=0;r<n;r++)i[e[r]]=t[r];else for(r=0;r<n;r++)i[r]=t[r];return i}var wl="lusolve",Nl=["typed","matrix","lup","slu","usolve","lsolve","DenseMatrix"],Ol=Object(s.a)(wl,Nl,function(e){var t=e.typed,n=e.matrix,i=e.lup,a=e.slu,s=e.usolve,u=e.lsolve,c=eo({DenseMatrix:e.DenseMatrix});return t(wl,{"Array, Array | Matrix":function(e,t){e=n(e);var r=i(e);return o(r.L,r.U,r.p,null,t).valueOf()},"DenseMatrix, Array | Matrix":function(e,t){var r=i(e);return o(r.L,r.U,r.p,null,t)},"SparseMatrix, Array | Matrix":function(e,t){var r=i(e);return o(r.L,r.U,r.p,null,t)},"SparseMatrix, Array | Matrix, number, number":function(e,t,r,n){var i=a(e,r,n);return o(i.L,i.U,i.p,i.q,t)},"Object, Array | Matrix":function(e,t){return o(e.L,e.U,e.p,e.q,t)}});function f(e){if(Object(ie.v)(e))return e;if(Object(ie.b)(e))return n(e);throw new TypeError("Invalid Matrix LU decomposition")}function o(e,t,r,n,i){e=f(e),t=f(t),i=c(e,i,!1),r&&(i._data=xl(r,i._data));var a=u(e,i),o=s(t,a);return n&&(o._data=xl(n,o._data)),o}}),Ml=["parse"],El=Object(s.a)("Help",Ml,function(e){var o=e.parse;function n(e){if(!(this instanceof n))throw new SyntaxError("Constructor must be called with the new operator");if(!e)throw new Error('Argument "doc" missing');this.doc=e}return n.prototype.type="Help",n.prototype.isHelp=!0,n.prototype.toString=function(){var e=this.doc||{},t="\n";if(e.name&&(t+="Name: "+e.name+"\n\n"),e.category&&(t+="Category: "+e.category+"\n\n"),e.description&&(t+="Description:\n    "+e.description+"\n\n"),e.syntax&&(t+="Syntax:\n    "+e.syntax.join("\n    ")+"\n\n"),e.examples){t+="Examples:\n";for(var r={},n=0;n<e.examples.length;n++){var i=e.examples[n];t+="    "+i+"\n";var a=void 0;try{a=o(i).compile().evaluate(r)}catch(e){a=e}void 0===a||Object(ie.s)(a)||(t+="        "+Object(J.d)(a,{precision:14})+"\n")}t+="\n"}return e.seealso&&e.seealso.length&&(t+="See also: "+e.seealso.join(", ")+"\n"),t},n.prototype.toJSON=function(){var e=Object(ae.a)(this.doc);return e.mathjs="Help",e},n.fromJSON=function(e){var t={};for(var r in e)"mathjs"!==r&&(t[r]=e[r]);return new n(t)},n.prototype.valueOf=n.prototype.toString,n},{isClass:!0}),jl=["?on","math"],Sl=Object(s.a)("Chain",jl,function(e){var t=e.on,r=e.math;function i(e){if(!(this instanceof i))throw new SyntaxError("Constructor must be called with the new operator");Object(ie.h)(e)?this.value=e.value:this.value=e}function a(e,t){Object(ae.h)(i.prototype,e,function(){var e=t();if("function"==typeof e)return o(e)})}function o(r){return function(){for(var e=[this.value],t=0;t<arguments.length;t++)e[t+1]=arguments[t];return new i(r.apply(r,e))}}i.prototype.type="Chain",i.prototype.isChain=!0,i.prototype.done=function(){return this.value},i.prototype.valueOf=function(){return this.value},i.prototype.toString=function(){return Object(J.d)(this.value)},i.prototype.toJSON=function(){return{mathjs:"Chain",value:this.value}},i.fromJSON=function(e){return new i(e.value)},i.createProxy=function(t,e){if("string"==typeof t)!function(e,t){"function"==typeof t&&(i.prototype[e]=o(t))}(t,e);else{var r=function(e){Object(ae.f)(t,e)&&void 0===s[e]&&a(e,function(){return t[e]})};for(var n in t)r(n)}};var s={expression:!0,docs:!0,type:!0,classes:!0,json:!0,error:!0,isChain:!0};return i.createProxy(r),t&&t("import",function(e,t,r){r||a(e,t)}),i},{isClass:!0}),Al={name:"typeOf",category:"Utils",syntax:["typeOf(x)"],description:"Get the type of a variable.",examples:["typeOf(3.5)","typeOf(2 - 4i)","typeOf(45 deg)",'typeOf("hello world")'],seealso:["getMatrixDataType"]},Cl={name:"evaluate",category:"Expression",syntax:["evaluate(expression)","evaluate([expr1, expr2, expr3, ...])"],description:"Evaluate an expression or an array with expressions.",examples:['evaluate("2 + 3")','evaluate("sqrt(" + 4 + ")")'],seealso:[]},Tl={name:"pi",category:"Constants",syntax:["pi"],description:"The number pi is a mathematical constant that is the ratio of a circle's circumference to its diameter, and is approximately equal to 3.14159",examples:["pi","sin(pi/2)"],seealso:["tau"]},_l={name:"e",category:"Constants",syntax:["e"],description:"Euler's number, the base of the natural logarithm. Approximately equal to 2.71828",examples:["e","e ^ 2","exp(2)","log(e)"],seealso:["exp"]},Il={name:"variance",category:"Statistics",syntax:["variance(a, b, c, ...)","variance(A)","variance(A, normalization)"],description:'Compute the variance of all values. Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["variance(2, 4, 6)","variance([2, 4, 6, 8])",'variance([2, 4, 6, 8], "uncorrected")','variance([2, 4, 6, 8], "biased")',"variance([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","min","prod","std","sum"]},ql={bignumber:{name:"bignumber",category:"Construction",syntax:["bignumber(x)"],description:"Create a big number from a number or string.",examples:["0.1 + 0.2","bignumber(0.1) + bignumber(0.2)",'bignumber("7.2")','bignumber("7.2e500")',"bignumber([0.1, 0.2, 0.3])"],seealso:["boolean","complex","fraction","index","matrix","string","unit"]},boolean:{name:"boolean",category:"Construction",syntax:["x","boolean(x)"],description:"Convert a string or number into a boolean.",examples:["boolean(0)","boolean(1)","boolean(3)",'boolean("true")','boolean("false")',"boolean([1, 0, 1, 1])"],seealso:["bignumber","complex","index","matrix","number","string","unit"]},complex:{name:"complex",category:"Construction",syntax:["complex()","complex(re, im)","complex(string)"],description:"Create a complex number.",examples:["complex()","complex(2, 3)",'complex("7 - 2i")'],seealso:["bignumber","boolean","index","matrix","number","string","unit"]},createUnit:{name:"createUnit",category:"Construction",syntax:["createUnit(definitions)","createUnit(name, definition)"],description:"Create a user-defined unit and register it with the Unit type.",examples:['createUnit("foo")','createUnit("knot", {definition: "0.514444444 m/s", aliases: ["knots", "kt", "kts"]})','createUnit("mph", "1 mile/hour")'],seealso:["unit","splitUnit"]},fraction:{name:"fraction",category:"Construction",syntax:["fraction(num)","fraction(num,den)"],description:"Create a fraction from a number or from a numerator and denominator.",examples:["fraction(0.125)","fraction(1, 3) + fraction(2, 5)"],seealso:["bignumber","boolean","complex","index","matrix","string","unit"]},index:{name:"index",category:"Construction",syntax:["[start]","[start:end]","[start:step:end]","[start1, start 2, ...]","[start1:end1, start2:end2, ...]","[start1:step1:end1, start2:step2:end2, ...]"],description:"Create an index to get or replace a subset of a matrix",examples:["[]","[1, 2, 3]","A = [1, 2, 3; 4, 5, 6]","A[1, :]","A[1, 2] = 50","A[0:2, 0:2] = ones(2, 2)"],seealso:["bignumber","boolean","complex","matrix,","number","range","string","unit"]},matrix:{name:"matrix",category:"Construction",syntax:["[]","[a1, b1, ...; a2, b2, ...]","matrix()",'matrix("dense")',"matrix([...])"],description:"Create a matrix.",examples:["[]","[1, 2, 3]","[1, 2, 3; 4, 5, 6]","matrix()","matrix([3, 4])",'matrix([3, 4; 5, 6], "sparse")','matrix([3, 4; 5, 6], "sparse", "number")'],seealso:["bignumber","boolean","complex","index","number","string","unit","sparse"]},number:{name:"number",category:"Construction",syntax:["x","number(x)","number(unit, valuelessUnit)"],description:"Create a number or convert a string or boolean into a number.",examples:["2","2e3","4.05","number(2)",'number("7.2")',"number(true)","number([true, false, true, true])",'number(unit("52cm"), "m")'],seealso:["bignumber","boolean","complex","fraction","index","matrix","string","unit"]},sparse:{name:"sparse",category:"Construction",syntax:["sparse()","sparse([a1, b1, ...; a1, b2, ...])",'sparse([a1, b1, ...; a1, b2, ...], "number")'],description:"Create a sparse matrix.",examples:["sparse()","sparse([3, 4; 5, 6])",'sparse([3, 0; 5, 0], "number")'],seealso:["bignumber","boolean","complex","index","number","string","unit","matrix"]},splitUnit:{name:"splitUnit",category:"Construction",syntax:["splitUnit(unit: Unit, parts: Unit[])"],description:"Split a unit in an array of units whose sum is equal to the original unit.",examples:['splitUnit(1 m, ["feet", "inch"])'],seealso:["unit","createUnit"]},string:{name:"string",category:"Construction",syntax:['"text"',"string(x)"],description:"Create a string or convert a value to a string",examples:['"Hello World!"',"string(4.2)","string(3 + 2i)"],seealso:["bignumber","boolean","complex","index","matrix","number","unit"]},unit:{name:"unit",category:"Construction",syntax:["value unit","unit(value, unit)","unit(string)"],description:"Create a unit.",examples:["5.5 mm","3 inch",'unit(7.1, "kilogram")','unit("23 deg")'],seealso:["bignumber","boolean","complex","index","matrix","number","string"]},e:_l,E:_l,false:{name:"false",category:"Constants",syntax:["false"],description:"Boolean value false",examples:["false"],seealso:["true"]},i:{name:"i",category:"Constants",syntax:["i"],description:"Imaginary unit, defined as i*i=-1. A complex number is described as a + b*i, where a is the real part, and b is the imaginary part.",examples:["i","i * i","sqrt(-1)"],seealso:[]},Infinity:{name:"Infinity",category:"Constants",syntax:["Infinity"],description:"Infinity, a number which is larger than the maximum number that can be handled by a floating point number.",examples:["Infinity","1 / 0"],seealso:[]},LN2:{name:"LN2",category:"Constants",syntax:["LN2"],description:"Returns the natural logarithm of 2, approximately equal to 0.693",examples:["LN2","log(2)"],seealso:[]},LN10:{name:"LN10",category:"Constants",syntax:["LN10"],description:"Returns the natural logarithm of 10, approximately equal to 2.302",examples:["LN10","log(10)"],seealso:[]},LOG2E:{name:"LOG2E",category:"Constants",syntax:["LOG2E"],description:"Returns the base-2 logarithm of E, approximately equal to 1.442",examples:["LOG2E","log(e, 2)"],seealso:[]},LOG10E:{name:"LOG10E",category:"Constants",syntax:["LOG10E"],description:"Returns the base-10 logarithm of E, approximately equal to 0.434",examples:["LOG10E","log(e, 10)"],seealso:[]},NaN:{name:"NaN",category:"Constants",syntax:["NaN"],description:"Not a number",examples:["NaN","0 / 0"],seealso:[]},null:{name:"null",category:"Constants",syntax:["null"],description:"Value null",examples:["null"],seealso:["true","false"]},pi:Tl,PI:Tl,phi:{name:"phi",category:"Constants",syntax:["phi"],description:"Phi is the golden ratio. Two quantities are in the golden ratio if their ratio is the same as the ratio of their sum to the larger of the two quantities. Phi is defined as `(1 + sqrt(5)) / 2` and is approximately 1.618034...",examples:["phi"],seealso:[]},SQRT1_2:{name:"SQRT1_2",category:"Constants",syntax:["SQRT1_2"],description:"Returns the square root of 1/2, approximately equal to 0.707",examples:["SQRT1_2","sqrt(1/2)"],seealso:[]},SQRT2:{name:"SQRT2",category:"Constants",syntax:["SQRT2"],description:"Returns the square root of 2, approximately equal to 1.414",examples:["SQRT2","sqrt(2)"],seealso:[]},tau:{name:"tau",category:"Constants",syntax:["tau"],description:"Tau is the ratio constant of a circle's circumference to radius, equal to 2 * pi, approximately 6.2832.",examples:["tau","2 * pi"],seealso:["pi"]},true:{name:"true",category:"Constants",syntax:["true"],description:"Boolean value true",examples:["true"],seealso:["false"]},version:{name:"version",category:"Constants",syntax:["version"],description:"A string with the version number of math.js",examples:["version"],seealso:[]},speedOfLight:{description:"Speed of light in vacuum",examples:["speedOfLight"]},gravitationConstant:{description:"Newtonian constant of gravitation",examples:["gravitationConstant"]},planckConstant:{description:"Planck constant",examples:["planckConstant"]},reducedPlanckConstant:{description:"Reduced Planck constant",examples:["reducedPlanckConstant"]},magneticConstant:{description:"Magnetic constant (vacuum permeability)",examples:["magneticConstant"]},electricConstant:{description:"Electric constant (vacuum permeability)",examples:["electricConstant"]},vacuumImpedance:{description:"Characteristic impedance of vacuum",examples:["vacuumImpedance"]},coulomb:{description:"Coulomb's constant",examples:["coulomb"]},elementaryCharge:{description:"Elementary charge",examples:["elementaryCharge"]},bohrMagneton:{description:"Borh magneton",examples:["bohrMagneton"]},conductanceQuantum:{description:"Conductance quantum",examples:["conductanceQuantum"]},inverseConductanceQuantum:{description:"Inverse conductance quantum",examples:["inverseConductanceQuantum"]},magneticFluxQuantum:{description:"Magnetic flux quantum",examples:["magneticFluxQuantum"]},nuclearMagneton:{description:"Nuclear magneton",examples:["nuclearMagneton"]},klitzing:{description:"Von Klitzing constant",examples:["klitzing"]},bohrRadius:{description:"Borh radius",examples:["bohrRadius"]},classicalElectronRadius:{description:"Classical electron radius",examples:["classicalElectronRadius"]},electronMass:{description:"Electron mass",examples:["electronMass"]},fermiCoupling:{description:"Fermi coupling constant",examples:["fermiCoupling"]},fineStructure:{description:"Fine-structure constant",examples:["fineStructure"]},hartreeEnergy:{description:"Hartree energy",examples:["hartreeEnergy"]},protonMass:{description:"Proton mass",examples:["protonMass"]},deuteronMass:{description:"Deuteron Mass",examples:["deuteronMass"]},neutronMass:{description:"Neutron mass",examples:["neutronMass"]},quantumOfCirculation:{description:"Quantum of circulation",examples:["quantumOfCirculation"]},rydberg:{description:"Rydberg constant",examples:["rydberg"]},thomsonCrossSection:{description:"Thomson cross section",examples:["thomsonCrossSection"]},weakMixingAngle:{description:"Weak mixing angle",examples:["weakMixingAngle"]},efimovFactor:{description:"Efimov factor",examples:["efimovFactor"]},atomicMass:{description:"Atomic mass constant",examples:["atomicMass"]},avogadro:{description:"Avogadro's number",examples:["avogadro"]},boltzmann:{description:"Boltzmann constant",examples:["boltzmann"]},faraday:{description:"Faraday constant",examples:["faraday"]},firstRadiation:{description:"First radiation constant",examples:["firstRadiation"]},loschmidt:{description:"Loschmidt constant at T=273.15 K and p=101.325 kPa",examples:["loschmidt"]},gasConstant:{description:"Gas constant",examples:["gasConstant"]},molarPlanckConstant:{description:"Molar Planck constant",examples:["molarPlanckConstant"]},molarVolume:{description:"Molar volume of an ideal gas at T=273.15 K and p=101.325 kPa",examples:["molarVolume"]},sackurTetrode:{description:"Sackur-Tetrode constant at T=1 K and p=101.325 kPa",examples:["sackurTetrode"]},secondRadiation:{description:"Second radiation constant",examples:["secondRadiation"]},stefanBoltzmann:{description:"Stefan-Boltzmann constant",examples:["stefanBoltzmann"]},wienDisplacement:{description:"Wien displacement law constant",examples:["wienDisplacement"]},molarMass:{description:"Molar mass constant",examples:["molarMass"]},molarMassC12:{description:"Molar mass constant of carbon-12",examples:["molarMassC12"]},gravity:{description:"Standard acceleration of gravity (standard acceleration of free-fall on Earth)",examples:["gravity"]},planckLength:{description:"Planck length",examples:["planckLength"]},planckMass:{description:"Planck mass",examples:["planckMass"]},planckTime:{description:"Planck time",examples:["planckTime"]},planckCharge:{description:"Planck charge",examples:["planckCharge"]},planckTemperature:{description:"Planck temperature",examples:["planckTemperature"]},derivative:{name:"derivative",category:"Algebra",syntax:["derivative(expr, variable)","derivative(expr, variable, {simplify: boolean})"],description:"Takes the derivative of an expression expressed in parser Nodes. The derivative will be taken over the supplied variable in the second parameter. If there are multiple variables in the expression, it will return a partial derivative.",examples:['derivative("2x^3", "x")','derivative("2x^3", "x", {simplify: false})','derivative("2x^2 + 3x + 4", "x")','derivative("sin(2x)", "x")','f = parse("x^2 + x")','x = parse("x")',"df = derivative(f, x)","df.evaluate({x: 3})"],seealso:["simplify","parse","evaluate"]},lsolve:{name:"lsolve",category:"Algebra",syntax:["x=lsolve(L, b)"],description:"Solves the linear system L * x = b where L is an [n x n] lower triangular matrix and b is a [n] column vector.",examples:["a = [-2, 3; 2, 1]","b = [11, 9]","x = lsolve(a, b)"],seealso:["lup","lusolve","usolve","matrix","sparse"]},lup:{name:"lup",category:"Algebra",syntax:["lup(m)"],description:"Calculate the Matrix LU decomposition with partial pivoting. Matrix A is decomposed in three matrices (L, U, P) where P * A = L * U",examples:["lup([[2, 1], [1, 4]])","lup(matrix([[2, 1], [1, 4]]))","lup(sparse([[2, 1], [1, 4]]))"],seealso:["lusolve","lsolve","usolve","matrix","sparse","slu","qr"]},lusolve:{name:"lusolve",category:"Algebra",syntax:["x=lusolve(A, b)","x=lusolve(lu, b)"],description:"Solves the linear system A * x = b where A is an [n x n] matrix and b is a [n] column vector.",examples:["a = [-2, 3; 2, 1]","b = [11, 9]","x = lusolve(a, b)"],seealso:["lup","slu","lsolve","usolve","matrix","sparse"]},simplify:{name:"simplify",category:"Algebra",syntax:["simplify(expr)","simplify(expr, rules)"],description:"Simplify an expression tree.",examples:['simplify("3 + 2 / 4")','simplify("2x + x")','f = parse("x * (x + 2 + x)")',"simplified = simplify(f)","simplified.evaluate({x: 2})"],seealso:["derivative","parse","evaluate"]},rationalize:{name:"rationalize",category:"Algebra",syntax:["rationalize(expr)","rationalize(expr, scope)","rationalize(expr, scope, detailed)"],description:"Transform a rationalizable expression in a rational fraction. If rational fraction is one variable polynomial then converts the numerator and denominator in canonical form, with decreasing exponents, returning the coefficients of numerator.",examples:['rationalize("2x/y - y/(x+1)")','rationalize("2x/y - y/(x+1)", true)'],seealso:["simplify"]},slu:{name:"slu",category:"Algebra",syntax:["slu(A, order, threshold)"],description:"Calculate the Matrix LU decomposition with full pivoting. Matrix A is decomposed in two matrices (L, U) and two permutation vectors (pinv, q) where P * A * Q = L * U",examples:["slu(sparse([4.5, 0, 3.2, 0; 3.1, 2.9, 0, 0.9; 0, 1.7, 3, 0; 3.5, 0.4, 0, 1]), 1, 0.001)"],seealso:["lusolve","lsolve","usolve","matrix","sparse","lup","qr"]},usolve:{name:"usolve",category:"Algebra",syntax:["x=usolve(U, b)"],description:"Solves the linear system U * x = b where U is an [n x n] upper triangular matrix and b is a [n] column vector.",examples:["x=usolve(sparse([1, 1, 1, 1; 0, 1, 1, 1; 0, 0, 1, 1; 0, 0, 0, 1]), [1; 2; 3; 4])"],seealso:["lup","lusolve","lsolve","matrix","sparse"]},qr:{name:"qr",category:"Algebra",syntax:["qr(A)"],description:"Calculates the Matrix QR decomposition. Matrix `A` is decomposed in two matrices (`Q`, `R`) where `Q` is an orthogonal matrix and `R` is an upper triangular matrix.",examples:["qr([[1, -1,  4], [1,  4, -2], [1,  4,  2], [1,  -1, 0]])"],seealso:["lup","slu","matrix"]},abs:{name:"abs",category:"Arithmetic",syntax:["abs(x)"],description:"Compute the absolute value.",examples:["abs(3.5)","abs(-4.2)"],seealso:["sign"]},add:{name:"add",category:"Operators",syntax:["x + y","add(x, y)"],description:"Add two values.",examples:["a = 2.1 + 3.6","a - 3.6","3 + 2i","3 cm + 2 inch",'"2.3" + "4"'],seealso:["subtract"]},cbrt:{name:"cbrt",category:"Arithmetic",syntax:["cbrt(x)","cbrt(x, allRoots)"],description:"Compute the cubic root value. If x = y * y * y, then y is the cubic root of x. When `x` is a number or complex number, an optional second argument `allRoots` can be provided to return all three cubic roots. If not provided, the principal root is returned",examples:["cbrt(64)","cube(4)","cbrt(-8)","cbrt(2 + 3i)","cbrt(8i)","cbrt(8i, true)","cbrt(27 m^3)"],seealso:["square","sqrt","cube","multiply"]},ceil:{name:"ceil",category:"Arithmetic",syntax:["ceil(x)"],description:"Round a value towards plus infinity. If x is complex, both real and imaginary part are rounded towards plus infinity.",examples:["ceil(3.2)","ceil(3.8)","ceil(-4.2)"],seealso:["floor","fix","round"]},cube:{name:"cube",category:"Arithmetic",syntax:["cube(x)"],description:"Compute the cube of a value. The cube of x is x * x * x.",examples:["cube(2)","2^3","2 * 2 * 2"],seealso:["multiply","square","pow"]},divide:{name:"divide",category:"Operators",syntax:["x / y","divide(x, y)"],description:"Divide two values.",examples:["a = 2 / 3","a * 3","4.5 / 2","3 + 4 / 2","(3 + 4) / 2","18 km / 4.5"],seealso:["multiply"]},dotDivide:{name:"dotDivide",category:"Operators",syntax:["x ./ y","dotDivide(x, y)"],description:"Divide two values element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","b = [2, 1, 1; 3, 2, 5]","a ./ b"],seealso:["multiply","dotMultiply","divide"]},dotMultiply:{name:"dotMultiply",category:"Operators",syntax:["x .* y","dotMultiply(x, y)"],description:"Multiply two values element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","b = [2, 1, 1; 3, 2, 5]","a .* b"],seealso:["multiply","divide","dotDivide"]},dotPow:{name:"dotPow",category:"Operators",syntax:["x .^ y","dotPow(x, y)"],description:"Calculates the power of x to y element wise.",examples:["a = [1, 2, 3; 4, 5, 6]","a .^ 2"],seealso:["pow"]},exp:{name:"exp",category:"Arithmetic",syntax:["exp(x)"],description:"Calculate the exponent of a value.",examples:["exp(1.3)","e ^ 1.3","log(exp(1.3))","x = 2.4","(exp(i*x) == cos(x) + i*sin(x))   # Euler's formula"],seealso:["expm","expm1","pow","log"]},expm:{name:"expm",category:"Arithmetic",syntax:["exp(x)"],description:"Compute the matrix exponential, expm(A) = e^A. The matrix must be square. Not to be confused with exp(a), which performs element-wise exponentiation.",examples:["expm([[0,2],[0,0]])"],seealso:["exp"]},expm1:{name:"expm1",category:"Arithmetic",syntax:["expm1(x)"],description:"Calculate the value of subtracting 1 from the exponential value.",examples:["expm1(2)","pow(e, 2) - 1","log(expm1(2) + 1)"],seealso:["exp","pow","log"]},fix:{name:"fix",category:"Arithmetic",syntax:["fix(x)"],description:"Round a value towards zero. If x is complex, both real and imaginary part are rounded towards zero.",examples:["fix(3.2)","fix(3.8)","fix(-4.2)","fix(-4.8)"],seealso:["ceil","floor","round"]},floor:{name:"floor",category:"Arithmetic",syntax:["floor(x)"],description:"Round a value towards minus infinity.If x is complex, both real and imaginary part are rounded towards minus infinity.",examples:["floor(3.2)","floor(3.8)","floor(-4.2)"],seealso:["ceil","fix","round"]},gcd:{name:"gcd",category:"Arithmetic",syntax:["gcd(a, b)","gcd(a, b, c, ...)"],description:"Compute the greatest common divisor.",examples:["gcd(8, 12)","gcd(-4, 6)","gcd(25, 15, -10)"],seealso:["lcm","xgcd"]},hypot:{name:"hypot",category:"Arithmetic",syntax:["hypot(a, b, c, ...)","hypot([a, b, c, ...])"],description:"Calculate the hypotenusa of a list with values. ",examples:["hypot(3, 4)","sqrt(3^2 + 4^2)","hypot(-2)","hypot([3, 4, 5])"],seealso:["abs","norm"]},lcm:{name:"lcm",category:"Arithmetic",syntax:["lcm(x, y)"],description:"Compute the least common multiple.",examples:["lcm(4, 6)","lcm(6, 21)","lcm(6, 21, 5)"],seealso:["gcd"]},log:{name:"log",category:"Arithmetic",syntax:["log(x)","log(x, base)"],description:"Compute the logarithm of a value. If no base is provided, the natural logarithm of x is calculated. If base if provided, the logarithm is calculated for the specified base. log(x, base) is defined as log(x) / log(base).",examples:["log(3.5)","a = log(2.4)","exp(a)","10 ^ 4","log(10000, 10)","log(10000) / log(10)","b = log(1024, 2)","2 ^ b"],seealso:["exp","log1p","log2","log10"]},log2:{name:"log2",category:"Arithmetic",syntax:["log2(x)"],description:"Calculate the 2-base of a value. This is the same as calculating `log(x, 2)`.",examples:["log2(0.03125)","log2(16)","log2(16) / log2(2)","pow(2, 4)"],seealso:["exp","log1p","log","log10"]},log1p:{name:"log1p",category:"Arithmetic",syntax:["log1p(x)","log1p(x, base)"],description:"Calculate the logarithm of a `value+1`",examples:["log1p(2.5)","exp(log1p(1.4))","pow(10, 4)","log1p(9999, 10)","log1p(9999) / log(10)"],seealso:["exp","log","log2","log10"]},log10:{name:"log10",category:"Arithmetic",syntax:["log10(x)"],description:"Compute the 10-base logarithm of a value.",examples:["log10(0.00001)","log10(10000)","10 ^ 4","log(10000) / log(10)","log(10000, 10)"],seealso:["exp","log"]},mod:{name:"mod",category:"Operators",syntax:["x % y","x mod y","mod(x, y)"],description:"Calculates the modulus, the remainder of an integer division.",examples:["7 % 3","11 % 2","10 mod 4","isOdd(x) = x % 2","isOdd(2)","isOdd(3)"],seealso:["divide"]},multiply:{name:"multiply",category:"Operators",syntax:["x * y","multiply(x, y)"],description:"multiply two values.",examples:["a = 2.1 * 3.4","a / 3.4","2 * 3 + 4","2 * (3 + 4)","3 * 2.1 km"],seealso:["divide"]},norm:{name:"norm",category:"Arithmetic",syntax:["norm(x)","norm(x, p)"],description:"Calculate the norm of a number, vector or matrix.",examples:["abs(-3.5)","norm(-3.5)","norm(3 - 4i)","norm([1, 2, -3], Infinity)","norm([1, 2, -3], -Infinity)","norm([3, 4], 2)","norm([[1, 2], [3, 4]], 1)",'norm([[1, 2], [3, 4]], "inf")','norm([[1, 2], [3, 4]], "fro")']},nthRoot:{name:"nthRoot",category:"Arithmetic",syntax:["nthRoot(a)","nthRoot(a, root)"],description:'Calculate the nth root of a value. The principal nth root of a positive real number A, is the positive real solution of the equation "x^root = A".',examples:["4 ^ 3","nthRoot(64, 3)","nthRoot(9, 2)","sqrt(9)"],seealso:["nthRoots","pow","sqrt"]},nthRoots:{name:"nthRoots",category:"Arithmetic",syntax:["nthRoots(A)","nthRoots(A, root)"],description:'Calculate the nth roots of a value. An nth root of a positive real number A, is a positive real solution of the equation "x^root = A". This function returns an array of complex values.',examples:["nthRoots(1)","nthRoots(1, 3)"],seealso:["sqrt","pow","nthRoot"]},pow:{name:"pow",category:"Operators",syntax:["x ^ y","pow(x, y)"],description:"Calculates the power of x to y, x^y.",examples:["2^3","2*2*2","1 + e ^ (pi * i)"],seealso:["multiply","nthRoot","nthRoots","sqrt"]},round:{name:"round",category:"Arithmetic",syntax:["round(x)","round(x, n)"],description:"round a value towards the nearest integer.If x is complex, both real and imaginary part are rounded towards the nearest integer. When n is specified, the value is rounded to n decimals.",examples:["round(3.2)","round(3.8)","round(-4.2)","round(-4.8)","round(pi, 3)","round(123.45678, 2)"],seealso:["ceil","floor","fix"]},sign:{name:"sign",category:"Arithmetic",syntax:["sign(x)"],description:"Compute the sign of a value. The sign of a value x is 1 when x>1, -1 when x<0, and 0 when x=0.",examples:["sign(3.5)","sign(-4.2)","sign(0)"],seealso:["abs"]},sqrt:{name:"sqrt",category:"Arithmetic",syntax:["sqrt(x)"],description:"Compute the square root value. If x = y * y, then y is the square root of x.",examples:["sqrt(25)","5 * 5","sqrt(-1)"],seealso:["square","sqrtm","multiply","nthRoot","nthRoots","pow"]},sqrtm:{name:"sqrtm",category:"Arithmetic",syntax:["sqrtm(x)"],description:"Calculate the principal square root of a square matrix. The principal square root matrix `X` of another matrix `A` is such that `X * X = A`.",examples:["sqrtm([[1, 2], [3, 4]])"],seealso:["sqrt","abs","square","multiply"]},square:{name:"square",category:"Arithmetic",syntax:["square(x)"],description:"Compute the square of a value. The square of x is x * x.",examples:["square(3)","sqrt(9)","3^2","3 * 3"],seealso:["multiply","pow","sqrt","cube"]},subtract:{name:"subtract",category:"Operators",syntax:["x - y","subtract(x, y)"],description:"subtract two values.",examples:["a = 5.3 - 2","a + 2","2/3 - 1/6","2 * 3 - 3","2.1 km - 500m"],seealso:["add"]},unaryMinus:{name:"unaryMinus",category:"Operators",syntax:["-x","unaryMinus(x)"],description:"Inverse the sign of a value. Converts booleans and strings to numbers.",examples:["-4.5","-(-5.6)",'-"22"'],seealso:["add","subtract","unaryPlus"]},unaryPlus:{name:"unaryPlus",category:"Operators",syntax:["+x","unaryPlus(x)"],description:"Converts booleans and strings to numbers.",examples:["+true",'+"2"'],seealso:["add","subtract","unaryMinus"]},xgcd:{name:"xgcd",category:"Arithmetic",syntax:["xgcd(a, b)"],description:"Calculate the extended greatest common divisor for two values. The result is an array [d, x, y] with 3 entries, where d is the greatest common divisor, and d = x * a + y * b.",examples:["xgcd(8, 12)","gcd(8, 12)","xgcd(36163, 21199)"],seealso:["gcd","lcm"]},bitAnd:{name:"bitAnd",category:"Bitwise",syntax:["x & y","bitAnd(x, y)"],description:"Bitwise AND operation. Performs the logical AND operation on each pair of the corresponding bits of the two given values by multiplying them. If both bits in the compared position are 1, the bit in the resulting binary representation is 1, otherwise, the result is 0",examples:["5 & 3","bitAnd(53, 131)","[1, 12, 31] & 42"],seealso:["bitNot","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitNot:{name:"bitNot",category:"Bitwise",syntax:["~x","bitNot(x)"],description:"Bitwise NOT operation. Performs a logical negation on each bit of the given value. Bits that are 0 become 1, and those that are 1 become 0.",examples:["~1","~2","bitNot([2, -3, 4])"],seealso:["bitAnd","bitOr","bitXor","leftShift","rightArithShift","rightLogShift"]},bitOr:{name:"bitOr",category:"Bitwise",syntax:["x | y","bitOr(x, y)"],description:"Bitwise OR operation. Performs the logical inclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if the first bit is 1 or the second bit is 1 or both bits are 1, otherwise, the result is 0.",examples:["5 | 3","bitOr([1, 2, 3], 4)"],seealso:["bitAnd","bitNot","bitXor","leftShift","rightArithShift","rightLogShift"]},bitXor:{name:"bitXor",category:"Bitwise",syntax:["bitXor(x, y)"],description:"Bitwise XOR operation, exclusive OR. Performs the logical exclusive OR operation on each pair of corresponding bits of the two given values. The result in each position is 1 if only the first bit is 1 or only the second bit is 1, but will be 0 if both are 0 or both are 1.",examples:["bitOr(1, 2)","bitXor([2, 3, 4], 4)"],seealso:["bitAnd","bitNot","bitOr","leftShift","rightArithShift","rightLogShift"]},leftShift:{name:"leftShift",category:"Bitwise",syntax:["x << y","leftShift(x, y)"],description:"Bitwise left logical shift of a value x by y number of bits.",examples:["4 << 1","8 >> 1"],seealso:["bitAnd","bitNot","bitOr","bitXor","rightArithShift","rightLogShift"]},rightArithShift:{name:"rightArithShift",category:"Bitwise",syntax:["x >> y","rightArithShift(x, y)"],description:"Bitwise right arithmetic shift of a value x by y number of bits.",examples:["8 >> 1","4 << 1","-12 >> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightLogShift"]},rightLogShift:{name:"rightLogShift",category:"Bitwise",syntax:["x >>> y","rightLogShift(x, y)"],description:"Bitwise right logical shift of a value x by y number of bits.",examples:["8 >>> 1","4 << 1","-12 >>> 2"],seealso:["bitAnd","bitNot","bitOr","bitXor","leftShift","rightArithShift"]},bellNumbers:{name:"bellNumbers",category:"Combinatorics",syntax:["bellNumbers(n)"],description:"The Bell Numbers count the number of partitions of a set. A partition is a pairwise disjoint subset of S whose union is S. `bellNumbers` only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["bellNumbers(3)","bellNumbers(8)"],seealso:["stirlingS2"]},catalan:{name:"catalan",category:"Combinatorics",syntax:["catalan(n)"],description:"The Catalan Numbers enumerate combinatorial structures of many different types. catalan only takes integer arguments. The following condition must be enforced: n >= 0.",examples:["catalan(3)","catalan(8)"],seealso:["bellNumbers"]},composition:{name:"composition",category:"Combinatorics",syntax:["composition(n, k)"],description:"The composition counts of n into k parts. composition only takes integer arguments. The following condition must be enforced: k <= n.",examples:["composition(5, 3)"],seealso:["combinations"]},stirlingS2:{name:"stirlingS2",category:"Combinatorics",syntax:["stirlingS2(n, k)"],description:"he Stirling numbers of the second kind, counts the number of ways to partition a set of n labelled objects into k nonempty unlabelled subsets. `stirlingS2` only takes integer arguments. The following condition must be enforced: k <= n. If n = k or k = 1, then s(n,k) = 1.",examples:["stirlingS2(5, 3)"],seealso:["bellNumbers"]},config:{name:"config",category:"Core",syntax:["config()","config(options)"],description:"Get configuration or change configuration.",examples:["config()","1/3 + 1/4",'config({number: "Fraction"})',"1/3 + 1/4"],seealso:[]},import:{name:"import",category:"Core",syntax:["import(functions)","import(functions, options)"],description:"Import functions or constants from an object.",examples:["import({myFn: f(x)=x^2, myConstant: 32 })","myFn(2)","myConstant"],seealso:[]},typed:{name:"typed",category:"Core",syntax:["typed(signatures)","typed(name, signatures)"],description:"Create a typed function.",examples:['double = typed({ "number, number": f(x)=x+x })',"double(2)",'double("hello")'],seealso:[]},arg:{name:"arg",category:"Complex",syntax:["arg(x)"],description:"Compute the argument of a complex value. If x = a+bi, the argument is computed as atan2(b, a).",examples:["arg(2 + 2i)","atan2(3, 2)","arg(2 + 3i)"],seealso:["re","im","conj","abs"]},conj:{name:"conj",category:"Complex",syntax:["conj(x)"],description:"Compute the complex conjugate of a complex value. If x = a+bi, the complex conjugate is a-bi.",examples:["conj(2 + 3i)","conj(2 - 3i)","conj(-5.2i)"],seealso:["re","im","abs","arg"]},re:{name:"re",category:"Complex",syntax:["re(x)"],description:"Get the real part of a complex number.",examples:["re(2 + 3i)","im(2 + 3i)","re(-5.2i)","re(2.4)"],seealso:["im","conj","abs","arg"]},im:{name:"im",category:"Complex",syntax:["im(x)"],description:"Get the imaginary part of a complex number.",examples:["im(2 + 3i)","re(2 + 3i)","im(-5.2i)","im(2.4)"],seealso:["re","conj","abs","arg"]},evaluate:Cl,eval:Cl,help:{name:"help",category:"Expression",syntax:["help(object)","help(string)"],description:"Display documentation on a function or data type.",examples:["help(sqrt)",'help("complex")'],seealso:[]},distance:{name:"distance",category:"Geometry",syntax:["distance([x1, y1], [x2, y2])","distance([[x1, y1], [x2, y2])"],description:"Calculates the Euclidean distance between two points.",examples:["distance([0,0], [4,4])","distance([[0,0], [4,4]])"],seealso:[]},intersect:{name:"intersect",category:"Geometry",syntax:["intersect(expr1, expr2, expr3, expr4)","intersect(expr1, expr2, expr3)"],description:"Computes the intersection point of lines and/or planes.",examples:["intersect([0, 0], [10, 10], [10, 0], [0, 10])","intersect([1, 0, 1],  [4, -2, 2], [1, 1, 1, 6])"],seealso:[]},and:{name:"and",category:"Logical",syntax:["x and y","and(x, y)"],description:"Logical and. Test whether two values are both defined with a nonzero/nonempty value.",examples:["true and false","true and true","2 and 4"],seealso:["not","or","xor"]},not:{name:"not",category:"Logical",syntax:["not x","not(x)"],description:"Logical not. Flips the boolean value of given argument.",examples:["not true","not false","not 2","not 0"],seealso:["and","or","xor"]},or:{name:"or",category:"Logical",syntax:["x or y","or(x, y)"],description:"Logical or. Test if at least one value is defined with a nonzero/nonempty value.",examples:["true or false","false or false","0 or 4"],seealso:["not","and","xor"]},xor:{name:"xor",category:"Logical",syntax:["x xor y","xor(x, y)"],description:"Logical exclusive or, xor. Test whether one and only one value is defined with a nonzero/nonempty value.",examples:["true xor false","false xor false","true xor true","0 xor 4"],seealso:["not","and","or"]},concat:{name:"concat",category:"Matrix",syntax:["concat(A, B, C, ...)","concat(A, B, C, ..., dim)"],description:"Concatenate matrices. By default, the matrices are concatenated by the last dimension. The dimension on which to concatenate can be provided as last argument.",examples:["A = [1, 2; 5, 6]","B = [3, 4; 7, 8]","concat(A, B)","concat(A, B, 1)","concat(A, B, 2)"],seealso:["det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},cross:{name:"cross",category:"Matrix",syntax:["cross(A, B)"],description:"Calculate the cross product for two vectors in three dimensional space.",examples:["cross([1, 1, 0],  [0, 1, 1])","cross([3, -3, 1], [4, 9, 2])","cross([2, 3, 4],  [5, 6, 7])"],seealso:["multiply","dot"]},column:{name:"column",category:"Matrix",syntax:["column(x, index)"],description:"Return a column from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","column(A, 1)","column(A, 2)"],seealso:["row"]},ctranspose:{name:"ctranspose",category:"Matrix",syntax:["x'","ctranspose(x)"],description:"Complex Conjugate and Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","ctranspose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},det:{name:"det",category:"Matrix",syntax:["det(x)"],description:"Calculate the determinant of a matrix",examples:["det([1, 2; 3, 4])","det([-2, 2, 3; -1, 1, 3; 2, 0, -1])"],seealso:["concat","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},diag:{name:"diag",category:"Matrix",syntax:["diag(x)","diag(x, k)"],description:"Create a diagonal matrix or retrieve the diagonal of a matrix. When x is a vector, a matrix with the vector values on the diagonal will be returned. When x is a matrix, a vector with the diagonal values of the matrix is returned. When k is provided, the k-th diagonal will be filled in or retrieved, if k is positive, the values are placed on the super diagonal. When k is negative, the values are placed on the sub diagonal.",examples:["diag(1:3)","diag(1:3, 1)","a = [1, 2, 3; 4, 5, 6; 7, 8, 9]","diag(a)"],seealso:["concat","det","identity","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},dot:{name:"dot",category:"Matrix",syntax:["dot(A, B)","A * B"],description:"Calculate the dot product of two vectors. The dot product of A = [a1, a2, a3, ..., an] and B = [b1, b2, b3, ..., bn] is defined as dot(A, B) = a1 * b1 + a2 * b2 + a3 * b3 + ... + an * bn",examples:["dot([2, 4, 1], [2, 2, 3])","[2, 4, 1] * [2, 2, 3]"],seealso:["multiply","cross"]},getMatrixDataType:{name:"getMatrixDataType",category:"Matrix",syntax:["getMatrixDataType(x)"],description:'Find the data type of all elements in a matrix or array, for example "number" if all items are a number and "Complex" if all values are complex numbers. If a matrix contains more than one data type, it will return "mixed".',examples:["getMatrixDataType([1, 2, 3])","getMatrixDataType([[5 cm], [2 inch]])",'getMatrixDataType([1, "text"])',"getMatrixDataType([1, bignumber(4)])"],seealso:["matrix","sparse","typeOf"]},identity:{name:"identity",category:"Matrix",syntax:["identity(n)","identity(m, n)","identity([m, n])"],description:"Returns the identity matrix with size m-by-n. The matrix has ones on the diagonal and zeros elsewhere.",examples:["identity(3)","identity(3, 5)","a = [1, 2, 3; 4, 5, 6]","identity(size(a))"],seealso:["concat","det","diag","inv","ones","range","size","squeeze","subset","trace","transpose","zeros"]},filter:{name:"filter",category:"Matrix",syntax:["filter(x, test)"],description:"Filter items in a matrix.",examples:["isPositive(x) = x > 0","filter([6, -2, -1, 4, 3], isPositive)","filter([6, -2, 0, 1, 0], x != 0)"],seealso:["sort","map","forEach"]},flatten:{name:"flatten",category:"Matrix",syntax:["flatten(x)"],description:"Flatten a multi dimensional matrix into a single dimensional matrix.",examples:["a = [1, 2, 3; 4, 5, 6]","size(a)","b = flatten(a)","size(b)"],seealso:["concat","resize","size","squeeze"]},forEach:{name:"forEach",category:"Matrix",syntax:["forEach(x, callback)"],description:"Iterates over all elements of a matrix/array, and executes the given callback function.",examples:["forEach([1, 2, 3], function(val) { console.log(val) })"],seealso:["map","sort","filter"]},inv:{name:"inv",category:"Matrix",syntax:["inv(x)"],description:"Calculate the inverse of a matrix",examples:["inv([1, 2; 3, 4])","inv(4)","1 / 4"],seealso:["concat","det","diag","identity","ones","range","size","squeeze","subset","trace","transpose","zeros"]},kron:{name:"kron",category:"Matrix",syntax:["kron(x, y)"],description:"Calculates the kronecker product of 2 matrices or vectors.",examples:["kron([[1, 0], [0, 1]], [[1, 2], [3, 4]])","kron([1,1], [2,3,4])"],seealso:["multiply","dot","cross"]},map:{name:"map",category:"Matrix",syntax:["map(x, callback)"],description:"Create a new matrix or array with the results of the callback function executed on each entry of the matrix/array.",examples:["map([1, 2, 3], square)"],seealso:["filter","forEach"]},ones:{name:"ones",category:"Matrix",syntax:["ones(m)","ones(m, n)","ones(m, n, p, ...)","ones([m])","ones([m, n])","ones([m, n, p, ...])"],description:"Create a matrix containing ones.",examples:["ones(3)","ones(3, 5)","ones([2,3]) * 4.5","a = [1, 2, 3; 4, 5, 6]","ones(size(a))"],seealso:["concat","det","diag","identity","inv","range","size","squeeze","subset","trace","transpose","zeros"]},partitionSelect:{name:"partitionSelect",category:"Matrix",syntax:["partitionSelect(x, k)","partitionSelect(x, k, compare)"],description:"Partition-based selection of an array or 1D matrix. Will find the kth smallest value, and mutates the input array. Uses Quickselect.",examples:["partitionSelect([5, 10, 1], 2)",'partitionSelect(["C", "B", "A", "D"], 1)'],seealso:["sort"]},range:{name:"range",category:"Type",syntax:["start:end","start:step:end","range(start, end)","range(start, end, step)","range(string)"],description:"Create a range. Lower bound of the range is included, upper bound is excluded.",examples:["1:5","3:-1:-3","range(3, 7)","range(0, 12, 2)",'range("4:10")',"a = [1, 2, 3, 4; 5, 6, 7, 8]","a[1:2, 1:2]"],seealso:["concat","det","diag","identity","inv","ones","size","squeeze","subset","trace","transpose","zeros"]},resize:{name:"resize",category:"Matrix",syntax:["resize(x, size)","resize(x, size, defaultValue)"],description:"Resize a matrix.",examples:["resize([1,2,3,4,5], [3])","resize([1,2,3], [5])","resize([1,2,3], [5], -1)","resize(2, [2, 3])",'resize("hello", [8], "!")'],seealso:["size","subset","squeeze","reshape"]},reshape:{name:"reshape",category:"Matrix",syntax:["reshape(x, sizes)"],description:"Reshape a multi dimensional array to fit the specified dimensions.",examples:["reshape([1, 2, 3, 4, 5, 6], [2, 3])","reshape([[1, 2], [3, 4]], [1, 4])","reshape([[1, 2], [3, 4]], [4])"],seealso:["size","squeeze","resize"]},row:{name:"row",category:"Matrix",syntax:["row(x, index)"],description:"Return a row from a matrix or array.",examples:["A = [[1, 2], [3, 4]]","row(A, 1)","row(A, 2)"],seealso:["column"]},size:{name:"size",category:"Matrix",syntax:["size(x)"],description:"Calculate the size of a matrix.",examples:["size(2.3)",'size("hello world")',"a = [1, 2; 3, 4; 5, 6]","size(a)","size(1:6)"],seealso:["concat","det","diag","identity","inv","ones","range","squeeze","subset","trace","transpose","zeros"]},sort:{name:"sort",category:"Matrix",syntax:["sort(x)","sort(x, compare)"],description:'Sort the items in a matrix. Compare can be a string "asc", "desc", "natural", or a custom sort function.',examples:["sort([5, 10, 1])",'sort(["C", "B", "A", "D"])',"sortByLength(a, b) = size(a)[1] - size(b)[1]",'sort(["Langdon", "Tom", "Sara"], sortByLength)','sort(["10", "1", "2"], "natural")'],seealso:["map","filter","forEach"]},squeeze:{name:"squeeze",category:"Matrix",syntax:["squeeze(x)"],description:"Remove inner and outer singleton dimensions from a matrix.",examples:["a = zeros(3,2,1)","size(squeeze(a))","b = zeros(1,1,3)","size(squeeze(b))"],seealso:["concat","det","diag","identity","inv","ones","range","size","subset","trace","transpose","zeros"]},subset:{name:"subset",category:"Matrix",syntax:["value(index)","value(index) = replacement","subset(value, [index])","subset(value, [index], replacement)"],description:"Get or set a subset of a matrix or string. Indexes are one-based. Both the ranges lower-bound and upper-bound are included.",examples:["d = [1, 2; 3, 4]","e = []","e[1, 1:2] = [5, 6]","e[2, :] = [7, 8]","f = d * e","f[2, 1]","f[:, 1]"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","trace","transpose","zeros"]},trace:{name:"trace",category:"Matrix",syntax:["trace(A)"],description:"Calculate the trace of a matrix: the sum of the elements on the main diagonal of a square matrix.",examples:["A = [1, 2, 3; -1, 2, 3; 2, 0, 3]","trace(A)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","transpose","zeros"]},transpose:{name:"transpose",category:"Matrix",syntax:["x'","transpose(x)"],description:"Transpose a matrix",examples:["a = [1, 2, 3; 4, 5, 6]","a'","transpose(a)"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","zeros"]},zeros:{name:"zeros",category:"Matrix",syntax:["zeros(m)","zeros(m, n)","zeros(m, n, p, ...)","zeros([m])","zeros([m, n])","zeros([m, n, p, ...])"],description:"Create a matrix containing zeros.",examples:["zeros(3)","zeros(3, 5)","a = [1, 2, 3; 4, 5, 6]","zeros(size(a))"],seealso:["concat","det","diag","identity","inv","ones","range","size","squeeze","subset","trace","transpose"]},combinations:{name:"combinations",category:"Probability",syntax:["combinations(n, k)"],description:"Compute the number of combinations of n items taken k at a time",examples:["combinations(7, 5)"],seealso:["combinationsWithRep","permutations","factorial"]},combinationsWithRep:{name:"combinationsWithRep",category:"Probability",syntax:["combinationsWithRep(n, k)"],description:"Compute the number of combinations of n items taken k at a time with replacements.",examples:["combinationsWithRep(7, 5)"],seealso:["combinations","permutations","factorial"]},factorial:{name:"factorial",category:"Probability",syntax:["n!","factorial(n)"],description:"Compute the factorial of a value",examples:["5!","5 * 4 * 3 * 2 * 1","3!"],seealso:["combinations","combinationsWithRep","permutations","gamma"]},gamma:{name:"gamma",category:"Probability",syntax:["gamma(n)"],description:"Compute the gamma function. For small values, the Lanczos approximation is used, and for large values the extended Stirling approximation.",examples:["gamma(4)","3!","gamma(1/2)","sqrt(pi)"],seealso:["factorial"]},kldivergence:{name:"kldivergence",category:"Probability",syntax:["kldivergence(x, y)"],description:"Calculate the Kullback-Leibler (KL) divergence  between two distributions.",examples:["kldivergence([0.7,0.5,0.4], [0.2,0.9,0.5])"],seealso:[]},multinomial:{name:"multinomial",category:"Probability",syntax:["multinomial(A)"],description:"Multinomial Coefficients compute the number of ways of picking a1, a2, ..., ai unordered outcomes from `n` possibilities. multinomial takes one array of integers as an argument. The following condition must be enforced: every ai > 0.",examples:["multinomial([1, 2, 1])"],seealso:["combinations","factorial"]},permutations:{name:"permutations",category:"Probability",syntax:["permutations(n)","permutations(n, k)"],description:"Compute the number of permutations of n items taken k at a time",examples:["permutations(5)","permutations(5, 3)"],seealso:["combinations","combinationsWithRep","factorial"]},pickRandom:{name:"pickRandom",category:"Probability",syntax:["pickRandom(array)","pickRandom(array, number)","pickRandom(array, weights)","pickRandom(array, number, weights)","pickRandom(array, weights, number)"],description:"Pick a random entry from a given array.",examples:["pickRandom(0:10)","pickRandom([1, 3, 1, 6])","pickRandom([1, 3, 1, 6], 2)","pickRandom([1, 3, 1, 6], [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], 2, [2, 3, 2, 1])","pickRandom([1, 3, 1, 6], [2, 3, 2, 1], 2)"],seealso:["random","randomInt"]},random:{name:"random",category:"Probability",syntax:["random()","random(max)","random(min, max)","random(size)","random(size, max)","random(size, min, max)"],description:"Return a random number.",examples:["random()","random(10, 20)","random([2, 3])"],seealso:["pickRandom","randomInt"]},randomInt:{name:"randomInt",category:"Probability",syntax:["randomInt(max)","randomInt(min, max)","randomInt(size)","randomInt(size, max)","randomInt(size, min, max)"],description:"Return a random integer number",examples:["randomInt(10, 20)","randomInt([2, 3], 10)"],seealso:["pickRandom","random"]},compare:{name:"compare",category:"Relational",syntax:["compare(x, y)"],description:"Compare two values. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compare(2, 3)","compare(3, 2)","compare(2, 2)","compare(5cm, 40mm)","compare(2, [1, 2, 3])"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compareNatural","compareText"]},compareNatural:{name:"compareNatural",category:"Relational",syntax:["compareNatural(x, y)"],description:"Compare two values of any type in a deterministic, natural way. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:["compareNatural(2, 3)","compareNatural(3, 2)","compareNatural(2, 2)","compareNatural(5cm, 40mm)",'compareNatural("2", "10")',"compareNatural(2 + 3i, 2 + 4i)","compareNatural([1, 2, 4], [1, 2, 3])","compareNatural([1, 5], [1, 2, 3])","compareNatural([1, 2], [1, 2])","compareNatural({a: 2}, {a: 4})"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare","compareText"]},compareText:{name:"compareText",category:"Relational",syntax:["compareText(x, y)"],description:"Compare two strings lexically. Comparison is case sensitive. Returns 1 when x > y, -1 when x < y, and 0 when x == y.",examples:['compareText("B", "A")','compareText("A", "B")','compareText("A", "A")','compareText("2", "10")','compare("2", "10")',"compare(2, 10)",'compareNatural("2", "10")','compareText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural"]},deepEqual:{name:"deepEqual",category:"Relational",syntax:["deepEqual(x, y)"],description:"Check equality of two matrices element wise. Returns true if the size of both matrices is equal and when and each of the elements are equal.",examples:["deepEqual([1,3,4], [1,3,4])","deepEqual([1,3,4], [1,3])"],seealso:["equal","unequal","smaller","larger","smallerEq","largerEq","compare"]},equal:{name:"equal",category:"Relational",syntax:["x == y","equal(x, y)"],description:"Check equality of two values. Returns true if the values are equal, and false if not.",examples:["2+2 == 3","2+2 == 4","a = 3.2","b = 6-2.8","a == b","50cm == 0.5m"],seealso:["unequal","smaller","larger","smallerEq","largerEq","compare","deepEqual","equalText"]},equalText:{name:"equalText",category:"Relational",syntax:["equalText(x, y)"],description:"Check equality of two strings. Comparison is case sensitive. Returns true if the values are equal, and false if not.",examples:['equalText("Hello", "Hello")','equalText("a", "A")','equal("2e3", "2000")','equalText("2e3", "2000")','equalText("B", ["A", "B", "C"])'],seealso:["compare","compareNatural","compareText","equal"]},larger:{name:"larger",category:"Relational",syntax:["x > y","larger(x, y)"],description:"Check if value x is larger than y. Returns true if x is larger than y, and false if not.",examples:["2 > 3","5 > 2*2","a = 3.3","b = 6-2.8","(a > b)","(b < a)","5 cm > 2 inch"],seealso:["equal","unequal","smaller","smallerEq","largerEq","compare"]},largerEq:{name:"largerEq",category:"Relational",syntax:["x >= y","largerEq(x, y)"],description:"Check if value x is larger or equal to y. Returns true if x is larger or equal to y, and false if not.",examples:["2 >= 1+1","2 > 1+1","a = 3.2","b = 6-2.8","(a >= b)"],seealso:["equal","unequal","smallerEq","smaller","compare"]},smaller:{name:"smaller",category:"Relational",syntax:["x < y","smaller(x, y)"],description:"Check if value x is smaller than value y. Returns true if x is smaller than y, and false if not.",examples:["2 < 3","5 < 2*2","a = 3.3","b = 6-2.8","(a < b)","5 cm < 2 inch"],seealso:["equal","unequal","larger","smallerEq","largerEq","compare"]},smallerEq:{name:"smallerEq",category:"Relational",syntax:["x <= y","smallerEq(x, y)"],description:"Check if value x is smaller or equal to value y. Returns true if x is smaller than y, and false if not.",examples:["2 <= 1+1","2 < 1+1","a = 3.2","b = 6-2.8","(a <= b)"],seealso:["equal","unequal","larger","smaller","largerEq","compare"]},unequal:{name:"unequal",category:"Relational",syntax:["x != y","unequal(x, y)"],description:"Check unequality of two values. Returns true if the values are unequal, and false if they are equal.",examples:["2+2 != 3","2+2 != 4","a = 3.2","b = 6-2.8","a != b","50cm != 0.5m","5 cm != 2 inch"],seealso:["equal","smaller","larger","smallerEq","largerEq","compare","deepEqual"]},setCartesian:{name:"setCartesian",category:"Set",syntax:["setCartesian(set1, set2)"],description:"Create the cartesian product of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setCartesian([1, 2], [3, 4])"],seealso:["setUnion","setIntersect","setDifference","setPowerset"]},setDifference:{name:"setDifference",category:"Set",syntax:["setDifference(set1, set2)"],description:"Create the difference of two (multi)sets: every element of set1, that is not the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setDifference([1, 2, 3, 4], [3, 4, 5, 6])","setDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setSymDifference"]},setDistinct:{name:"setDistinct",category:"Set",syntax:["setDistinct(set)"],description:"Collect the distinct elements of a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setDistinct([1, 1, 1, 2, 2, 3])"],seealso:["setMultiplicity"]},setIntersect:{name:"setIntersect",category:"Set",syntax:["setIntersect(set1, set2)"],description:"Create the intersection of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIntersect([1, 2, 3, 4], [3, 4, 5, 6])","setIntersect([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setDifference"]},setIsSubset:{name:"setIsSubset",category:"Set",syntax:["setIsSubset(set1, set2)"],description:"Check whether a (multi)set is a subset of another (multi)set: every element of set1 is the element of set2. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setIsSubset([1, 2], [3, 4, 5, 6])","setIsSubset([3, 4], [3, 4, 5, 6])"],seealso:["setUnion","setIntersect","setDifference"]},setMultiplicity:{name:"setMultiplicity",category:"Set",syntax:["setMultiplicity(element, set)"],description:"Count the multiplicity of an element in a multiset. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setMultiplicity(1, [1, 2, 2, 4])","setMultiplicity(2, [1, 2, 2, 4])"],seealso:["setDistinct","setSize"]},setPowerset:{name:"setPowerset",category:"Set",syntax:["setPowerset(set)"],description:"Create the powerset of a (multi)set: the powerset contains very possible subsets of a (multi)set. A multi-dimension array will be converted to a single-dimension array before the operation.",examples:["setPowerset([1, 2, 3])"],seealso:["setCartesian"]},setSize:{name:"setSize",category:"Set",syntax:["setSize(set)","setSize(set, unique)"],description:'Count the number of elements of a (multi)set. When the second parameter "unique" is true, count only the unique values. A multi-dimension array will be converted to a single-dimension array before the operation.',examples:["setSize([1, 2, 2, 4])","setSize([1, 2, 2, 4], true)"],seealso:["setUnion","setIntersect","setDifference"]},setSymDifference:{name:"setSymDifference",category:"Set",syntax:["setSymDifference(set1, set2)"],description:"Create the symmetric difference of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setSymDifference([1, 2, 3, 4], [3, 4, 5, 6])","setSymDifference([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setUnion","setIntersect","setDifference"]},setUnion:{name:"setUnion",category:"Set",syntax:["setUnion(set1, set2)"],description:"Create the union of two (multi)sets. Multi-dimension arrays will be converted to single-dimension arrays before the operation.",examples:["setUnion([1, 2, 3, 4], [3, 4, 5, 6])","setUnion([[1, 2], [3, 4]], [[3, 4], [5, 6]])"],seealso:["setIntersect","setDifference"]},erf:{name:"erf",category:"Special",syntax:["erf(x)"],description:"Compute the erf function of a value using a rational Chebyshev approximations for different intervals of x",examples:["erf(0.2)","erf(-0.5)","erf(4)"],seealso:[]},mad:{name:"mad",category:"Statistics",syntax:["mad(a, b, c, ...)","mad(A)"],description:"Compute the median absolute deviation of a matrix or a list with values. The median absolute deviation is defined as the median of the absolute deviations from the median.",examples:["mad(10, 20, 30)","mad([1, 2, 3])"],seealso:["mean","median","std","abs"]},max:{name:"max",category:"Statistics",syntax:["max(a, b, c, ...)","max(A)","max(A, dim)"],description:"Compute the maximum value of a list of values.",examples:["max(2, 3, 4, 1)","max([2, 3, 4, 1])","max([2, 5; 4, 3])","max([2, 5; 4, 3], 1)","max([2, 5; 4, 3], 2)","max(2.7, 7.1, -4.5, 2.0, 4.1)","min(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["mean","median","min","prod","std","sum","variance"]},mean:{name:"mean",category:"Statistics",syntax:["mean(a, b, c, ...)","mean(A)","mean(A, dim)"],description:"Compute the arithmetic mean of a list of values.",examples:["mean(2, 3, 4, 1)","mean([2, 3, 4, 1])","mean([2, 5; 4, 3])","mean([2, 5; 4, 3], 1)","mean([2, 5; 4, 3], 2)","mean([1.0, 2.7, 3.2, 4.0])"],seealso:["max","median","min","prod","std","sum","variance"]},median:{name:"median",category:"Statistics",syntax:["median(a, b, c, ...)","median(A)"],description:"Compute the median of all values. The values are sorted and the middle value is returned. In case of an even number of values, the average of the two middle values is returned.",examples:["median(5, 2, 7)","median([3, -1, 5, 7])"],seealso:["max","mean","min","prod","std","sum","variance","quantileSeq"]},min:{name:"min",category:"Statistics",syntax:["min(a, b, c, ...)","min(A)","min(A, dim)"],description:"Compute the minimum value of a list of values.",examples:["min(2, 3, 4, 1)","min([2, 3, 4, 1])","min([2, 5; 4, 3])","min([2, 5; 4, 3], 1)","min([2, 5; 4, 3], 2)","min(2.7, 7.1, -4.5, 2.0, 4.1)","max(2.7, 7.1, -4.5, 2.0, 4.1)"],seealso:["max","mean","median","prod","std","sum","variance"]},mode:{name:"mode",category:"Statistics",syntax:["mode(a, b, c, ...)","mode(A)","mode(A, a, b, B, c, ...)"],description:"Computes the mode of all values as an array. In case mode being more than one, multiple values are returned in an array.",examples:["mode(2, 1, 4, 3, 1)","mode([1, 2.7, 3.2, 4, 2.7])","mode(1, 4, 6, 1, 6)"],seealso:["max","mean","min","median","prod","std","sum","variance"]},prod:{name:"prod",category:"Statistics",syntax:["prod(a, b, c, ...)","prod(A)"],description:"Compute the product of all values.",examples:["prod(2, 3, 4)","prod([2, 3, 4])","prod([2, 5; 4, 3])"],seealso:["max","mean","min","median","min","std","sum","variance"]},quantileSeq:{name:"quantileSeq",category:"Statistics",syntax:["quantileSeq(A, prob[, sorted])","quantileSeq(A, [prob1, prob2, ...][, sorted])","quantileSeq(A, N[, sorted])"],description:"Compute the prob order quantile of a matrix or a list with values. The sequence is sorted and the middle value is returned. Supported types of sequence values are: Number, BigNumber, Unit Supported types of probablity are: Number, BigNumber. \n\nIn case of a (multi dimensional) array or matrix, the prob order quantile of all elements will be calculated.",examples:["quantileSeq([3, -1, 5, 7], 0.5)","quantileSeq([3, -1, 5, 7], [1/3, 2/3])","quantileSeq([3, -1, 5, 7], 2)","quantileSeq([-1, 3, 5, 7], 0.5, true)"],seealso:["mean","median","min","max","prod","std","sum","variance"]},std:{name:"std",category:"Statistics",syntax:["std(a, b, c, ...)","std(A)","std(A, normalization)"],description:'Compute the standard deviation of all values, defined as std(A) = sqrt(variance(A)). Optional parameter normalization can be "unbiased" (default), "uncorrected", or "biased".',examples:["std(2, 4, 6)","std([2, 4, 6, 8])",'std([2, 4, 6, 8], "uncorrected")','std([2, 4, 6, 8], "biased")',"std([1, 2, 3; 4, 5, 6])"],seealso:["max","mean","min","median","prod","sum","variance"]},sum:{name:"sum",category:"Statistics",syntax:["sum(a, b, c, ...)","sum(A)"],description:"Compute the sum of all values.",examples:["sum(2, 3, 4, 1)","sum([2, 3, 4, 1])","sum([2, 5; 4, 3])"],seealso:["max","mean","median","min","prod","std","sum","variance"]},variance:Il,var:Il,acos:{name:"acos",category:"Trigonometry",syntax:["acos(x)"],description:"Compute the inverse cosine of a value in radians.",examples:["acos(0.5)","acos(cos(2.3))"],seealso:["cos","atan","asin"]},acosh:{name:"acosh",category:"Trigonometry",syntax:["acosh(x)"],description:"Calculate the hyperbolic arccos of a value, defined as `acosh(x) = ln(sqrt(x^2 - 1) + x)`.",examples:["acosh(1.5)"],seealso:["cosh","asinh","atanh"]},acot:{name:"acot",category:"Trigonometry",syntax:["acot(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acot(0.5)","acot(cot(0.5))","acot(2)"],seealso:["cot","atan"]},acoth:{name:"acoth",category:"Trigonometry",syntax:["acoth(x)"],description:"Calculate the hyperbolic arccotangent of a value, defined as `acoth(x) = (ln((x+1)/x) + ln(x/(x-1))) / 2`.",examples:["acoth(2)","acoth(0.5)"],seealso:["acsch","asech"]},acsc:{name:"acsc",category:"Trigonometry",syntax:["acsc(x)"],description:"Calculate the inverse cotangent of a value.",examples:["acsc(2)","acsc(csc(0.5))","acsc(0.5)"],seealso:["csc","asin","asec"]},acsch:{name:"acsch",category:"Trigonometry",syntax:["acsch(x)"],description:"Calculate the hyperbolic arccosecant of a value, defined as `acsch(x) = ln(1/x + sqrt(1/x^2 + 1))`.",examples:["acsch(0.5)"],seealso:["asech","acoth"]},asec:{name:"asec",category:"Trigonometry",syntax:["asec(x)"],description:"Calculate the inverse secant of a value.",examples:["asec(0.5)","asec(sec(0.5))","asec(2)"],seealso:["acos","acot","acsc"]},asech:{name:"asech",category:"Trigonometry",syntax:["asech(x)"],description:"Calculate the inverse secant of a value.",examples:["asech(0.5)"],seealso:["acsch","acoth"]},asin:{name:"asin",category:"Trigonometry",syntax:["asin(x)"],description:"Compute the inverse sine of a value in radians.",examples:["asin(0.5)","asin(sin(0.5))"],seealso:["sin","acos","atan"]},asinh:{name:"asinh",category:"Trigonometry",syntax:["asinh(x)"],description:"Calculate the hyperbolic arcsine of a value, defined as `asinh(x) = ln(x + sqrt(x^2 + 1))`.",examples:["asinh(0.5)"],seealso:["acosh","atanh"]},atan:{name:"atan",category:"Trigonometry",syntax:["atan(x)"],description:"Compute the inverse tangent of a value in radians.",examples:["atan(0.5)","atan(tan(0.5))"],seealso:["tan","acos","asin"]},atanh:{name:"atanh",category:"Trigonometry",syntax:["atanh(x)"],description:"Calculate the hyperbolic arctangent of a value, defined as `atanh(x) = ln((1 + x)/(1 - x)) / 2`.",examples:["atanh(0.5)"],seealso:["acosh","asinh"]},atan2:{name:"atan2",category:"Trigonometry",syntax:["atan2(y, x)"],description:"Computes the principal value of the arc tangent of y/x in radians.",examples:["atan2(2, 2) / pi","angle = 60 deg in rad","x = cos(angle)","y = sin(angle)","atan2(y, x)"],seealso:["sin","cos","tan"]},cos:{name:"cos",category:"Trigonometry",syntax:["cos(x)"],description:"Compute the cosine of x in radians.",examples:["cos(2)","cos(pi / 4) ^ 2","cos(180 deg)","cos(60 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["acos","sin","tan"]},cosh:{name:"cosh",category:"Trigonometry",syntax:["cosh(x)"],description:"Compute the hyperbolic cosine of x in radians.",examples:["cosh(0.5)"],seealso:["sinh","tanh","coth"]},cot:{name:"cot",category:"Trigonometry",syntax:["cot(x)"],description:"Compute the cotangent of x in radians. Defined as 1/tan(x)",examples:["cot(2)","1 / tan(2)"],seealso:["sec","csc","tan"]},coth:{name:"coth",category:"Trigonometry",syntax:["coth(x)"],description:"Compute the hyperbolic cotangent of x in radians.",examples:["coth(2)","1 / tanh(2)"],seealso:["sech","csch","tanh"]},csc:{name:"csc",category:"Trigonometry",syntax:["csc(x)"],description:"Compute the cosecant of x in radians. Defined as 1/sin(x)",examples:["csc(2)","1 / sin(2)"],seealso:["sec","cot","sin"]},csch:{name:"csch",category:"Trigonometry",syntax:["csch(x)"],description:"Compute the hyperbolic cosecant of x in radians. Defined as 1/sinh(x)",examples:["csch(2)","1 / sinh(2)"],seealso:["sech","coth","sinh"]},sec:{name:"sec",category:"Trigonometry",syntax:["sec(x)"],description:"Compute the secant of x in radians. Defined as 1/cos(x)",examples:["sec(2)","1 / cos(2)"],seealso:["cot","csc","cos"]},sech:{name:"sech",category:"Trigonometry",syntax:["sech(x)"],description:"Compute the hyperbolic secant of x in radians. Defined as 1/cosh(x)",examples:["sech(2)","1 / cosh(2)"],seealso:["coth","csch","cosh"]},sin:{name:"sin",category:"Trigonometry",syntax:["sin(x)"],description:"Compute the sine of x in radians.",examples:["sin(2)","sin(pi / 4) ^ 2","sin(90 deg)","sin(30 deg)","sin(0.2)^2 + cos(0.2)^2"],seealso:["asin","cos","tan"]},sinh:{name:"sinh",category:"Trigonometry",syntax:["sinh(x)"],description:"Compute the hyperbolic sine of x in radians.",examples:["sinh(0.5)"],seealso:["cosh","tanh"]},tan:{name:"tan",category:"Trigonometry",syntax:["tan(x)"],description:"Compute the tangent of x in radians.",examples:["tan(0.5)","sin(0.5) / cos(0.5)","tan(pi / 4)","tan(45 deg)"],seealso:["atan","sin","cos"]},tanh:{name:"tanh",category:"Trigonometry",syntax:["tanh(x)"],description:"Compute the hyperbolic tangent of x in radians.",examples:["tanh(0.5)","sinh(0.5) / cosh(0.5)"],seealso:["sinh","cosh"]},to:{name:"to",category:"Units",syntax:["x to unit","to(x, unit)"],description:"Change the unit of a value.",examples:["5 inch to cm","3.2kg to g","16 bytes in bits"],seealso:[]},clone:{name:"clone",category:"Utils",syntax:["clone(x)"],description:"Clone a variable. Creates a copy of primitive variables,and a deep copy of matrices",examples:["clone(3.5)","clone(2 - 4i)","clone(45 deg)","clone([1, 2; 3, 4])",'clone("hello world")'],seealso:[]},format:{name:"format",category:"Utils",syntax:["format(value)","format(value, precision)"],description:"Format a value of any type as string.",examples:["format(2.3)","format(3 - 4i)","format([])","format(pi, 3)"],seealso:["print"]},isNaN:{name:"isNaN",category:"Utils",syntax:["isNaN(x)"],description:"Test whether a value is NaN (not a number)",examples:["isNaN(2)","isNaN(0 / 0)","isNaN(NaN)","isNaN(Infinity)"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isInteger:{name:"isInteger",category:"Utils",syntax:["isInteger(x)"],description:"Test whether a value is an integer number.",examples:["isInteger(2)","isInteger(3.5)","isInteger([3, 0.5, -2])"],seealso:["isNegative","isNumeric","isPositive","isZero"]},isNegative:{name:"isNegative",category:"Utils",syntax:["isNegative(x)"],description:"Test whether a value is negative: smaller than zero.",examples:["isNegative(2)","isNegative(0)","isNegative(-4)","isNegative([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isPositive","isZero"]},isNumeric:{name:"isNumeric",category:"Utils",syntax:["isNumeric(x)"],description:"Test whether a value is a numeric value. Returns true when the input is a number, BigNumber, Fraction, or boolean.",examples:["isNumeric(2)",'isNumeric("2")','hasNumericValue("2")',"isNumeric(0)","isNumeric(bignumber(500))","isNumeric(fraction(0.125))","isNumeric(2 + 3i)",'isNumeric([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","hasNumericValue"]},hasNumericValue:{name:"hasNumericValue",category:"Utils",syntax:["hasNumericValue(x)"],description:"Test whether a value is an numeric value. In case of a string, true is returned if the string contains a numeric value.",examples:["hasNumericValue(2)",'hasNumericValue("2")','isNumeric("2")',"hasNumericValue(0)","hasNumericValue(bignumber(500))","hasNumericValue(fraction(0.125))","hasNumericValue(2 + 3i)",'hasNumericValue([2.3, "foo", false])'],seealso:["isInteger","isZero","isNegative","isPositive","isNaN","isNumeric"]},isPositive:{name:"isPositive",category:"Utils",syntax:["isPositive(x)"],description:"Test whether a value is positive: larger than zero.",examples:["isPositive(2)","isPositive(0)","isPositive(-4)","isPositive([3, 0.5, -2])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isPrime:{name:"isPrime",category:"Utils",syntax:["isPrime(x)"],description:"Test whether a value is prime: has no divisors other than itself and one.",examples:["isPrime(3)","isPrime(-2)","isPrime([2, 17, 100])"],seealso:["isInteger","isNumeric","isNegative","isZero"]},isZero:{name:"isZero",category:"Utils",syntax:["isZero(x)"],description:"Test whether a value is zero.",examples:["isZero(2)","isZero(0)","isZero(-4)","isZero([3, 0, -2, 0])"],seealso:["isInteger","isNumeric","isNegative","isPositive"]},typeOf:Al,typeof:Al,numeric:{name:"numeric",category:"Utils",syntax:["numeric(x)"],description:"Convert a numeric input to a specific numeric type: number, BigNumber, or Fraction.",examples:['numeric("4")','numeric("4", "number")','numeric("4", "BigNumber")','numeric("4", "Fraction)','numeric(4, "Fraction")','numeric(fraction(2, 5), "number)'],seealso:["number","fraction","bignumber","string","format"]}},Bl=["typed","mathWithTransform","Help"],kl=Object(s.a)("help",Bl,function(e){var t=e.typed,a=e.mathWithTransform,o=e.Help;return t("help",{any:function(e){var t,r=e;if("string"!=typeof e)for(t in a)if(Object(ae.f)(a,t)&&e===a[t]){r=t;break}var n=Fi(ql,r);if(n)return new o(n);var i="function"==typeof r?r.name:r;throw new Error('No documentation found on "'+i+'"')}})}),zl=["typed","Chain"],Dl=Object(s.a)("chain",zl,function(e){var t=e.typed,r=e.Chain;return t("chain",{"":function(){return new r},any:function(e){return new r(e)}})}),Rl=["typed","matrix","subtract","multiply","unaryMinus","lup"],Pl=Object(s.a)("det",Rl,function(e){var t=e.typed,n=e.matrix,f=e.subtract,l=e.multiply,p=e.unaryMinus,m=e.lup;return t("det",{any:function(e){return Object(ae.a)(e)},"Array | Matrix":function(e){var t;switch((t=Object(ie.v)(e)?e.size():Array.isArray(e)?(e=n(e)).size():[]).length){case 0:return Object(ae.a)(e);case 1:if(1===t[0])return Object(ae.a)(e.valueOf()[0]);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");case 2:var r=t[0];if(r===t[1])return function(e,t){{if(1===t)return Object(ae.a)(e[0][0]);if(2===t)return f(l(e[0][0],e[1][1]),l(e[1][0],e[0][1]));for(var r=m(e),n=r.U[0][0],i=1;i<t;i++)n=l(n,r.U[i][i]);for(var a=0,o=0,s=[];;){for(;s[o];)o++;if(t<=o)break;for(var u=o,c=0;!s[r.p[u]];)s[r.p[u]]=!0,u=r.p[u],c++;c%2==0&&a++}return a%2==0?n:p(n)}}(e.clone().valueOf(),r);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");default:throw new RangeError("Matrix must be two dimensional (size: "+Object(J.d)(t)+")")}}})}),Fl=["typed","matrix","divideScalar","addScalar","multiply","unaryMinus","det","identity","abs"],Ul=Object(s.a)("inv",Fl,function(e){var t=e.typed,i=e.matrix,v=e.divideScalar,b=e.addScalar,x=e.multiply,w=e.unaryMinus,N=e.det,O=e.identity,M=e.abs;return t("inv",{"Array | Matrix":function(e){var t=Object(ie.v)(e)?e.size():Object(I.a)(e);switch(t.length){case 1:if(1===t[0])return Object(ie.v)(e)?i([v(1,e.valueOf()[0])]):[v(1,e[0])];throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");case 2:var r=t[0],n=t[1];if(r===n)return Object(ie.v)(e)?i(a(e.valueOf(),r,n),e.storage()):a(e,r,n);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");default:throw new RangeError("Matrix must be two dimensional (size: "+Object(J.d)(t)+")")}},any:function(e){return v(1,e)}});function a(e,t,r){var n,i,a,o,s;if(1===t){if(0===(o=e[0][0]))throw Error("Cannot calculate inverse, determinant is zero");return[[v(1,o)]]}if(2===t){var u=N(e);if(0===u)throw Error("Cannot calculate inverse, determinant is zero");return[[v(e[1][1],u),v(w(e[0][1]),u)],[v(w(e[1][0]),u),v(e[0][0],u)]]}var c=e.concat();for(n=0;n<t;n++)c[n]=c[n].concat();for(var f=O(t).valueOf(),l=0;l<r;l++){var p=M(c[l][l]),m=l;for(n=l+1;n<t;)M(c[n][l])>p&&(p=M(c[n][l]),m=n),n++;if(0===p)throw Error("Cannot calculate inverse, determinant is zero");(n=m)!==l&&(s=c[l],c[l]=c[n],c[n]=s,s=f[l],f[l]=f[n],f[n]=s);var h=c[l],d=f[l];for(n=0;n<t;n++){var y=c[n],g=f[n];if(n!==l){if(0!==y[l]){for(a=v(w(y[l]),h[l]),i=l;i<r;i++)y[i]=b(y[i],x(a,h[i]));for(i=0;i<r;i++)g[i]=b(g[i],x(a,d[i]))}}else{for(a=h[l],i=l;i<r;i++)y[i]=v(y[i],a);for(i=0;i<r;i++)g[i]=v(g[i],a)}}}return f}}),Ll=["typed","abs","add","identity","inv","multiply"],Hl=Object(s.a)("expm",Ll,function(e){var t=e.typed,d=e.abs,y=e.add,g=e.identity,v=e.inv,b=e.multiply;return t("expm",{Matrix:function(e){var t=e.size();if(2!==t.length||t[0]!==t[1])throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");for(var r=t[0],n=function(e,t){for(var r=0;r<30;r++)for(var n=0;n<=r;n++){var i=r-n;if(x(e,n,i)<t)return{q:n,j:i}}throw new Error("Could not find acceptable parameters to compute the matrix exponential (try increasing maxSearchSize in expm.js)")}(function(e){for(var t=e.size()[0],r=0,n=0;n<t;n++){for(var i=0,a=0;a<t;a++)i+=d(e.get([n,a]));r=Math.max(i,r)}return r}(e),1e-15),i=n.q,a=n.j,o=b(e,Math.pow(2,-a)),s=g(r),u=g(r),c=1,f=o,l=-1,p=1;p<=i;p++)1<p&&(f=b(f,o),l=-l),s=y(s,b(c=c*(i-p+1)/((2*i-p+1)*p),f)),u=y(u,b(c*l,f));for(var m=b(v(u),s),h=0;h<a;h++)m=b(m,m);return Object(ie.H)(e)?e.createSparseMatrix(m):m}});function x(e,t,r){for(var n=1,i=2;i<=t;i++)n*=i;for(var a=n,o=t+1;o<=2*t;o++)a*=o;var s=a*(2*t+1);return 8*Math.pow(e/Math.pow(2,r),2*t)*n*n/(a*s)}}),$l=["typed","abs","add","multiply","sqrt","subtract","inv","size","max","identity"],Gl=Object(s.a)("sqrtm",$l,function(e){var t=e.typed,o=e.abs,s=e.add,u=e.multiply,r=e.sqrt,c=e.subtract,f=e.inv,l=e.size,p=e.max,m=e.identity,n=t("sqrtm",{"Array | Matrix":function(e){var t=Object(ie.v)(e)?e.size():Object(I.a)(e);switch(t.length){case 1:if(1===t[0])return r(e);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")");case 2:if(t[0]===t[1])return function(e){var t,r=0,n=e,i=m(l(e));do{var a=n;if(n=u(.5,s(a,f(i))),i=u(.5,s(i,f(a))),t=p(o(c(n,a))),d<t&&++r>h)throw new Error("computing square root of matrix: iterative method could not converge")}while(d<t);return n}(e);throw new RangeError("Matrix must be square (size: "+Object(J.d)(t)+")")}}}),h=1e3,d=1e-6;return n}),Zl=["typed","matrix","multiply","equalScalar","divideScalar","inv"],Vl=Object(s.a)("divide",Zl,function(e){var t=e.typed,r=e.matrix,n=e.multiply,i=e.equalScalar,a=e.divideScalar,o=e.inv,s=sr({typed:t,equalScalar:i}),u=Kt({typed:t});return t("divide",Object(ae.e)({"Array | Matrix, Array | Matrix":function(e,t){return n(e,o(t))},"DenseMatrix, any":function(e,t){return u(e,t,a,!1)},"SparseMatrix, any":function(e,t){return s(e,t,a,!1)},"Array, any":function(e,t){return u(r(e),t,a,!1).valueOf()},"any, Array | Matrix":function(e,t){return n(e,o(t))}},a.signatures))}),Jl="distance",Wl=["typed","addScalar","subtract","divideScalar","multiplyScalar","unaryMinus","sqrt","abs"],Yl=Object(s.a)(Jl,Wl,function(e){var t=e.typed,l=e.addScalar,p=e.subtract,m=e.multiplyScalar,h=e.divideScalar,s=e.unaryMinus,d=e.sqrt,u=e.abs;return t(Jl,{"Array, Array, Array":function(e,t,r){if(2!==e.length||2!==t.length||2!==r.length)throw new TypeError("Invalid Arguments: Try again");if(!c(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!c(t))throw new TypeError("Array with 2 numbers or BigNumbers expected for second argument");if(!c(r))throw new TypeError("Array with 2 numbers or BigNumbers expected for third argument");var n=h(p(r[1],r[0]),p(t[1],t[0])),i=m(m(n,n),t[0]),a=s(m(n,t[0])),o=e[1];return f(e[0],e[1],i,a,o)},"Object, Object, Object":function(e,t,r){if(2!==Object.keys(e).length||2!==Object.keys(t).length||2!==Object.keys(r).length)throw new TypeError("Invalid Arguments: Try again");if(!c(e))throw new TypeError("Values of pointX and pointY should be numbers or BigNumbers");if(!c(t))throw new TypeError("Values of lineOnePtX and lineOnePtY should be numbers or BigNumbers");if(!c(r))throw new TypeError("Values of lineTwoPtX and lineTwoPtY should be numbers or BigNumbers");if("pointX"in e&&"pointY"in e&&"lineOnePtX"in t&&"lineOnePtY"in t&&"lineTwoPtX"in r&&"lineTwoPtY"in r){var n=h(p(r.lineTwoPtY,r.lineTwoPtX),p(t.lineOnePtY,t.lineOnePtX)),i=m(m(n,n),t.lineOnePtX),a=s(m(n,t.lineOnePtX)),o=e.pointX;return f(e.pointX,e.pointY,i,a,o)}throw new TypeError("Key names do not match")},"Array, Array":function(e,t){if(2===e.length&&3===t.length){if(!c(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!n(t))throw new TypeError("Array with 3 numbers or BigNumbers expected for second argument");return f(e[0],e[1],t[0],t[1],t[2])}if(3===e.length&&6===t.length){if(!n(e))throw new TypeError("Array with 3 numbers or BigNumbers expected for first argument");if(!i(t))throw new TypeError("Array with 6 numbers or BigNumbers expected for second argument");return o(e[0],e[1],e[2],t[0],t[1],t[2],t[3],t[4],t[5])}if(2===e.length&&2===t.length){if(!c(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!c(t))throw new TypeError("Array with 2 numbers or BigNumbers expected for second argument");return y(e[0],e[1],t[0],t[1])}if(3!==e.length||3!==t.length)throw new TypeError("Invalid Arguments: Try again");if(!n(e))throw new TypeError("Array with 3 numbers or BigNumbers expected for first argument");if(!n(t))throw new TypeError("Array with 3 numbers or BigNumbers expected for second argument");return g(e[0],e[1],e[2],t[0],t[1],t[2])},"Object, Object":function(e,t){if(2===Object.keys(e).length&&3===Object.keys(t).length){if(!c(e))throw new TypeError("Values of pointX and pointY should be numbers or BigNumbers");if(!n(t))throw new TypeError("Values of xCoeffLine, yCoeffLine and constant should be numbers or BigNumbers");if("pointX"in e&&"pointY"in e&&"xCoeffLine"in t&&"yCoeffLine"in t&&"constant"in t)return f(e.pointX,e.pointY,t.xCoeffLine,t.yCoeffLine,t.constant);throw new TypeError("Key names do not match")}if(3===Object.keys(e).length&&6===Object.keys(t).length){if(!n(e))throw new TypeError("Values of pointX, pointY and pointZ should be numbers or BigNumbers");if(!i(t))throw new TypeError("Values of x0, y0, z0, a, b and c should be numbers or BigNumbers");if("pointX"in e&&"pointY"in e&&"x0"in t&&"y0"in t&&"z0"in t&&"a"in t&&"b"in t&&"c"in t)return o(e.pointX,e.pointY,e.pointZ,t.x0,t.y0,t.z0,t.a,t.b,t.c);throw new TypeError("Key names do not match")}if(2===Object.keys(e).length&&2===Object.keys(t).length){if(!c(e))throw new TypeError("Values of pointOneX and pointOneY should be numbers or BigNumbers");if(!c(t))throw new TypeError("Values of pointTwoX and pointTwoY should be numbers or BigNumbers");if("pointOneX"in e&&"pointOneY"in e&&"pointTwoX"in t&&"pointTwoY"in t)return y(e.pointOneX,e.pointOneY,t.pointTwoX,t.pointTwoY);throw new TypeError("Key names do not match")}if(3!==Object.keys(e).length||3!==Object.keys(t).length)throw new TypeError("Invalid Arguments: Try again");if(!n(e))throw new TypeError("Values of pointOneX, pointOneY and pointOneZ should be numbers or BigNumbers");if(!n(t))throw new TypeError("Values of pointTwoX, pointTwoY and pointTwoZ should be numbers or BigNumbers");if("pointOneX"in e&&"pointOneY"in e&&"pointOneZ"in e&&"pointTwoX"in t&&"pointTwoY"in t&&"pointTwoZ"in t)return g(e.pointOneX,e.pointOneY,e.pointOneZ,t.pointTwoX,t.pointTwoY,t.pointTwoZ);throw new TypeError("Key names do not match")},Array:function(e){if(!function(e){if(2===e[0].length&&r(e[0][0])&&r(e[0][1])){if(e.some(function(e){return 2!==e.length||!r(e[0])||!r(e[1])}))return!1}else{if(!(3===e[0].length&&r(e[0][0])&&r(e[0][1])&&r(e[0][2])))return!1;if(e.some(function(e){return 3!==e.length||!r(e[0])||!r(e[1])||!r(e[2])}))return!1}return!0}(e))throw new TypeError("Incorrect array format entered for pairwise distance calculation");return function(e){for(var t=[],r=0;r<e.length-1;r++)for(var n=r+1;n<e.length;n++)2===e[0].length?t.push(y(e[r][0],e[r][1],e[n][0],e[n][1])):3===e[0].length&&t.push(g(e[r][0],e[r][1],e[r][2],e[n][0],e[n][1],e[n][2]));return t}(e)}});function r(e){return"number"==typeof e||Object(ie.e)(e)}function c(e){return e.constructor!==Array&&(e=a(e)),r(e[0])&&r(e[1])}function n(e){return e.constructor!==Array&&(e=a(e)),r(e[0])&&r(e[1])&&r(e[2])}function i(e){return e.constructor!==Array&&(e=a(e)),r(e[0])&&r(e[1])&&r(e[2])&&r(e[3])&&r(e[4])&&r(e[5])}function a(e){for(var t=Object.keys(e),r=[],n=0;n<t.length;n++)r.push(e[t[n]]);return r}function f(e,t,r,n,i){var a=u(l(l(m(r,e),m(n,t)),i)),o=d(l(m(r,r),m(n,n)));return h(a,o)}function o(e,t,r,n,i,a,o,s,u){var c=[p(m(p(i,t),u),m(p(a,r),s)),p(m(p(a,r),o),m(p(n,e),u)),p(m(p(n,e),s),m(p(i,t),o))];c=d(l(l(m(c[0],c[0]),m(c[1],c[1])),m(c[2],c[2])));var f=d(l(l(m(o,o),m(s,s)),m(u,u)));return h(c,f)}function y(e,t,r,n){var i=p(n,t),a=p(r,e),o=l(m(i,i),m(a,a));return d(o)}function g(e,t,r,n,i,a){var o=p(a,r),s=p(i,t),u=p(n,e),c=l(l(m(o,o),m(s,s)),m(u,u));return d(c)}}),Xl=["typed","config","abs","add","addScalar","matrix","multiply","multiplyScalar","divideScalar","subtract","smaller","equalScalar"],Ql=Object(s.a)("intersect",Xl,function(e){var t=e.typed,h=e.config,d=e.abs,y=e.add,E=e.addScalar,i=e.matrix,g=e.multiply,j=e.multiplyScalar,S=e.divideScalar,A=e.subtract,v=e.smaller,C=e.equalScalar,a=t("intersect",{"Array, Array, Array":function(e,t,r){if(!s(e))throw new TypeError("Array with 3 numbers or BigNumbers expected for first argument");if(!s(t))throw new TypeError("Array with 3 numbers or BigNumbers expected for second argument");if(!function(e){return 4===e.length&&n(e[0])&&n(e[1])&&n(e[2])&&n(e[3])}(r))throw new TypeError("Array with 4 numbers expected as third argument");return function(e,t,r,n,i,a,o,s,u,c){var f=j(e,o),l=j(n,o),p=j(t,s),m=j(i,s),h=j(r,u),d=j(a,u),y=S(A(A(A(c,f),p),h),A(A(A(E(E(l,m),d),f),p),h)),g=E(e,j(y,A(n,e))),v=E(t,j(y,A(i,t))),b=E(r,j(y,A(a,r)));return[g,v,b]}(e[0],e[1],e[2],t[0],t[1],t[2],r[0],r[1],r[2],r[3])},"Array, Array, Array, Array":function(e,t,r,n){if(2===e.length){if(!o(e))throw new TypeError("Array with 2 numbers or BigNumbers expected for first argument");if(!o(t))throw new TypeError("Array with 2 numbers or BigNumbers expected for second argument");if(!o(r))throw new TypeError("Array with 2 numbers or BigNumbers expected for third argument");if(!o(n))throw new TypeError("Array with 2 numbers or BigNumbers expected for fourth argument");return function(e,t,r,n){var i=e,a=r,o=A(i,t),s=A(a,n),u=A(j(o[0],s[1]),j(s[0],o[1]));if(v(d(u),h.epsilon))return null;var c=j(s[0],i[1]),f=j(s[1],i[0]),l=j(s[0],a[1]),p=j(s[1],a[0]),m=S(E(A(A(c,f),l),p),u);return y(g(o,m),i)}(e,t,r,n)}if(3!==e.length)throw new TypeError("Arrays with two or thee dimensional points expected");if(!s(e))throw new TypeError("Array with 3 numbers or BigNumbers expected for first argument");if(!s(t))throw new TypeError("Array with 3 numbers or BigNumbers expected for second argument");if(!s(r))throw new TypeError("Array with 3 numbers or BigNumbers expected for third argument");if(!s(n))throw new TypeError("Array with 3 numbers or BigNumbers expected for fourth argument");return function(e,t,r,n,i,a,o,s,u,c,f,l){var p=T(e,o,c,o,t,s,f,s,r,u,l,u),m=T(c,o,n,e,f,s,i,t,l,u,a,r),h=T(e,o,n,e,t,s,i,t,r,u,a,r),d=T(c,o,c,o,f,s,f,s,l,u,l,u),y=T(n,e,n,e,i,t,i,t,a,r,a,r),g=S(A(j(p,m),j(h,d)),A(j(y,d),j(m,m))),v=S(E(p,j(g,m)),d),b=E(e,j(g,A(n,e))),x=E(t,j(g,A(i,t))),w=E(r,j(g,A(a,r))),N=E(o,j(v,A(c,o))),O=E(s,j(v,A(f,s))),M=E(u,j(v,A(l,u)));return C(b,N)&&C(x,O)&&C(w,M)?[b,x,w]:null}(e[0],e[1],e[2],t[0],t[1],t[2],r[0],r[1],r[2],n[0],n[1],n[2])},"Matrix, Matrix, Matrix":function(e,t,r){return i(a(e.valueOf(),t.valueOf(),r.valueOf()))},"Matrix, Matrix, Matrix, Matrix":function(e,t,r,n){return i(a(e.valueOf(),t.valueOf(),r.valueOf(),n.valueOf()))}});function n(e){return"number"==typeof e||Object(ie.e)(e)}function o(e){return 2===e.length&&n(e[0])&&n(e[1])}function s(e){return 3===e.length&&n(e[0])&&n(e[1])&&n(e[2])}function T(e,t,r,n,i,a,o,s,u,c,f,l){var p=j(A(e,t),A(r,n)),m=j(A(i,a),A(o,s)),h=j(A(u,c),A(f,l));return E(E(p,m),h)}return a}),Kl=["typed","config","add","?bignumber","?fraction"],ep=Object(s.a)("sum",Kl,function(e){var t=e.typed,n=e.config,i=e.add,a=e.bignumber,o=e.fraction;return t("sum",{"Array | Matrix":r,"Array | Matrix, number | BigNumber":function(e,t){try{return U(e,t,i)}catch(e){throw da(e,"sum")}},"...":function(e){if(P(e))throw new TypeError("Scalar values expected in function sum");return r(e)}});function r(e){var r;if(F(e,function(t){try{r=void 0===r?t:i(r,t)}catch(e){throw da(e,"sum",t)}}),void 0===r)switch(n.number){case"number":return 0;case"BigNumber":return a?a(0):wi();case"Fraction":return o?o(0):Ni();default:return 0}return r}}),tp=["typed","add","divide"],rp=Object(s.a)("mean",tp,function(e){var t=e.typed,i=e.add,a=e.divide;return t("mean",{"Array | Matrix":r,"Array | Matrix, number | BigNumber":function(e,t){try{var r=U(e,t,i),n=Array.isArray(e)?Object(I.a)(e):e.size();return a(r,n[t])}catch(e){throw da(e,"mean")}},"...":function(e){if(P(e))throw new TypeError("Scalar values expected in function mean");return r(e)}});function r(e){var r,n=0;if(F(e,function(t){try{r=void 0===r?t:i(r,t),n++}catch(e){throw da(e,"mean",t)}}),0===n)throw new Error("Cannot calculate the mean of an empty array");return a(r,n)}}),np=["typed","add","divide","compare","partitionSelect"],ip=Object(s.a)("median",np,function(e){var t=e.typed,r=e.add,n=e.divide,s=e.compare,u=e.partitionSelect,i=t("median",{"Array | Matrix":a,"Array | Matrix, number | BigNumber":function(e,t){throw new Error("median(A, dim) is not yet supported")},"...":function(e){if(P(e))throw new TypeError("Scalar values expected in function median");return a(e)}});function a(e){try{var t=(e=Object(I.e)(e.valueOf())).length;if(0===t)throw new Error("Cannot calculate median of an empty array");if(t%2==0){for(var r=t/2-1,n=u(e,1+r),i=e[r],a=0;a<r;++a)0<s(e[a],i)&&(i=e[a]);return f(i,n)}var o=u(e,(t-1)/2);return c(o)}catch(e){throw da(e,"median")}}var c=t({"number | BigNumber | Complex | Unit":function(e){return e}}),f=t({"number | BigNumber | Complex | Unit, number | BigNumber | Complex | Unit":function(e,t){return n(r(e,t),2)}});return i}),ap=["typed","abs","map","median","subtract"],op=Object(s.a)("mad",ap,function(e){var t=e.typed,r=e.abs,n=e.map,i=e.median,a=e.subtract;return t("mad",{"Array | Matrix":o,"...":function(e){return o(e)}});function o(e){if(0===(e=Object(I.e)(e.valueOf())).length)throw new Error("Cannot calculate median absolute deviation (mad) of an empty array");try{var t=i(e);return i(n(e,function(e){return r(a(e,t))}))}catch(e){throw e instanceof TypeError&&-1!==e.message.indexOf("median")?new TypeError(e.message.replace("median","mad")):da(e,"mad")}}}),sp="unbiased",up="variance",cp=["typed","add","subtract","multiply","divide","apply","isNaN"],fp=Object(s.a)(up,cp,function(e){var t=e.typed,o=e.add,s=e.subtract,u=e.multiply,c=e.divide,n=e.apply,f=e.isNaN;return t(up,{"Array | Matrix":function(e){return i(e,sp)},"Array | Matrix, string":i,"Array | Matrix, number | BigNumber":function(e,t){return r(e,t,sp)},"Array | Matrix, number | BigNumber, string":r,"...":function(e){return i(e,sp)}});function i(e,t){var r=0,n=0;if(0===e.length)throw new SyntaxError("Function variance requires one or more parameters (0 provided)");if(F(e,function(t){try{r=o(r,t),n++}catch(e){throw da(e,"variance",t)}}),0===n)throw new Error("Cannot calculate variance of an empty array");var i=c(r,n);if(r=0,F(e,function(e){var t=s(e,i);r=o(r,u(t,t))}),f(r))return r;switch(t){case"uncorrected":return c(r,n);case"biased":return c(r,n+1);case"unbiased":var a=Object(ie.e)(r)?r.mul(0):0;return 1===n?a:c(r,n-1);default:throw new Error('Unknown normalization "'+t+'". Choose "unbiased" (default), "uncorrected", or "biased".')}}function r(e,t,r){try{if(0===e.length)throw new SyntaxError("Function variance requires one or more parameters (0 provided)");return n(e,t,function(e){return i(e,r)})}catch(e){throw da(e,"variance")}}}),lp=Object(s.a)("var",["variance"],function(e){var n=e.variance;return function(){Object(ve.a)('Function "var" has been renamed to "variance" in v6.0.0, please use the new function instead.');for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return n.apply(n,t)}}),pp=["typed","add","multiply","partitionSelect","compare"],mp=Object(s.a)("quantileSeq",pp,function(e){var t=e.typed,w=e.add,N=e.multiply,O=e.partitionSelect,M=e.compare;function h(e,t,r){var n=Object(I.e)(e),i=n.length;if(0===i)throw new Error("Cannot calculate quantile of an empty sequence");if(Object(ie.y)(t)){var a=t*(i-1),o=a%1;if(0==o){var s=r?n[a]:O(n,a);return E(s),s}var u,c,f=Math.floor(a);if(r)u=n[f],c=n[f+1];else{c=O(n,f+1),u=n[f];for(var l=0;l<f;++l)0<M(n[l],u)&&(u=n[l])}return E(u),E(c),w(N(u,1-o),N(c,o))}var p=t.times(i-1);if(p.isInteger()){p=p.toNumber();var m=r?n[p]:O(n,p);return E(m),m}var h,d,y=p.floor(),g=p.minus(y),v=y.toNumber();if(r)h=n[v],d=n[v+1];else{d=O(n,v+1),h=n[v];for(var b=0;b<v;++b)0<M(n[b],h)&&(h=n[b])}E(h),E(d);var x=new g.constructor(1);return w(N(h,x.minus(g)),N(d,g))}var E=t({"number | BigNumber | Unit":function(e){return e}});return function(e,t,r){var n,i,a;if(arguments.length<2||3<arguments.length)throw new SyntaxError("Function quantileSeq requires two or three parameters");if(Object(ie.i)(e)){if("boolean"!=typeof(r=r||!1))throw new TypeError("Unexpected type of argument in function quantileSeq");if(i=e.valueOf(),Object(ie.y)(t)){if(t<0)throw new Error("N/prob must be non-negative");if(t<=1)return h(i,t,r);if(1<t){if(!Object(j.i)(t))throw new Error("N must be a positive integer");var o=t+1;n=new Array(t);for(var s=0;s<t;)n[s]=h(i,++s/o,r);return n}}if(Object(ie.e)(t)){var u=t.constructor;if(t.isNegative())throw new Error("N/prob must be non-negative");if(a=new u(1),t.lte(a))return new u(h(i,t,r));if(t.gt(a)){if(!t.isInteger())throw new Error("N must be a positive integer");var c=t.toNumber();if(4294967295<c)throw new Error("N must be less than or equal to 2^32-1, as that is the maximum length of an Array");var f=new u(c+1);n=new Array(c);for(var l=0;l<c;)n[l]=new u(h(i,new u(++l).div(f),r));return n}}if(Array.isArray(t)){n=new Array(t.length);for(var p=0;p<n.length;++p){var m=t[p];if(Object(ie.y)(m)){if(m<0||1<m)throw new Error("Probability must be between 0 and 1, inclusive")}else{if(!Object(ie.e)(m))throw new TypeError("Unexpected type of argument in function quantileSeq");if(a=new m.constructor(1),m.isNegative()||m.gt(a))throw new Error("Probability must be between 0 and 1, inclusive")}n[p]=h(i,m,r)}return n}throw new TypeError("Unexpected type of argument in function quantileSeq")}throw new TypeError("Unexpected type of argument in function quantileSeq")}}),hp=["typed","sqrt","variance"],dp=Object(s.a)("std",hp,function(e){var t=e.typed,r=e.sqrt,n=e.variance;return t("std",{"Array | Matrix":i,"Array | Matrix, string":i,"Array | Matrix, number | BigNumber":i,"Array | Matrix, number | BigNumber, string":i,"...":function(e){return i(e)}});function i(e,t){if(0===e.length)throw new SyntaxError("Function std requires one or more parameters (0 provided)");try{return r(n.apply(null,arguments))}catch(e){throw e instanceof TypeError&&-1!==e.message.indexOf(" variance")?new TypeError(e.message.replace(" variance"," std")):e}}});function yp(e,t){if(t<e)return 1;if(t===e)return t;var r=t+e>>1;return yp(e,r)*yp(1+r,t)}function gp(e,t){if(!Object(j.i)(e)||e<0)throw new TypeError("Positive integer value expected in function combinations");if(!Object(j.i)(t)||t<0)throw new TypeError("Positive integer value expected in function combinations");if(e<t)throw new TypeError("k must be less than or equal to n");var r=e-t;return t<r?yp(1+r,e)/yp(1,t):yp(t+1,e)/yp(1,r)}gp.signature="number, number";var vp="combinations",bp=["typed"],xp=Object(s.a)(vp,bp,function(e){return(0,e.typed)(vp,{"number, number":gp,"BigNumber, BigNumber":function(e,t){var r,n,i=e.constructor,a=e.minus(t),o=new i(1);if(!wp(e)||!wp(t))throw new TypeError("Positive integer value expected in function combinations");if(t.gt(e))throw new TypeError("k must be less than n in function combinations");if(r=o,t.lt(a))for(n=o;n.lte(a);n=n.plus(o))r=r.times(t.plus(n)).dividedBy(n);else for(n=o;n.lte(t);n=n.plus(o))r=r.times(a.plus(n)).dividedBy(n);return r}})});function wp(e){return e.isInteger()&&e.gte(0)}var Np="combinationsWithRep",Op=["typed"],Mp=Object(s.a)(Np,Op,function(e){return(0,e.typed)(Np,{"number, number":function(e,t){if(!Object(j.i)(e)||e<0)throw new TypeError("Positive integer value expected in function combinationsWithRep");if(!Object(j.i)(t)||t<0)throw new TypeError("Positive integer value expected in function combinationsWithRep");if(e<1)throw new TypeError("k must be less than or equal to n + k - 1");return t<e-1?yp(e,e+t-1)/yp(1,t):yp(t+1,e+t-1)/yp(1,e-1)},"BigNumber, BigNumber":function(e,t){var r,n,i=new e.constructor(1),a=e.minus(i);if(!Ep(e)||!Ep(t))throw new TypeError("Positive integer value expected in function combinationsWithRep");if(e.lt(i))throw new TypeError("k must be less than or equal to n + k - 1 in function combinationsWithRep");if(r=i,t.lt(a))for(n=i;n.lte(a);n=n.plus(i))r=r.times(t.plus(n)).dividedBy(n);else for(n=i;n.lte(t);n=n.plus(i))r=r.times(a.plus(n)).dividedBy(n);return r}})});function Ep(e){return e.isInteger()&&e.gte(0)}function jp(e){var t;if(Object(j.i)(e))return e<=0?isFinite(e)?1/0:NaN:171<e?1/0:yp(1,e-1);if(e<.5)return Math.PI/(Math.sin(Math.PI*e)*jp(1-e));if(171.35<=e)return 1/0;if(85<e){var r=e*e,n=r*e,i=n*e,a=i*e;return Math.sqrt(2*Math.PI/e)*Math.pow(e/Math.E,e)*(1+1/(12*e)+1/(288*r)-139/(51840*n)-571/(2488320*i)+163879/(209018880*a)+5246819/(75246796800*a*e))}--e,t=Ap[0];for(var o=1;o<Ap.length;++o)t+=Ap[o]/(e+o);var s=e+Sp+.5;return Math.sqrt(2*Math.PI)*Math.pow(s,e+.5)*Math.exp(-s)*t}jp.signature="number";var Sp=4.7421875,Ap=[.9999999999999971,57.15623566586292,-59.59796035547549,14.136097974741746,-.4919138160976202,3399464998481189e-20,4652362892704858e-20,-9837447530487956e-20,.0001580887032249125,-.00021026444172410488,.00021743961811521265,-.0001643181065367639,8441822398385275e-20,-26190838401581408e-21,36899182659531625e-22],Cp=["typed","config","multiplyScalar","pow","BigNumber","Complex"],Tp=Object(s.a)("gamma",Cp,function(e){var t=e.typed,i=e.config,c=e.multiplyScalar,f=e.pow,a=e.BigNumber,l=e.Complex,p=t("gamma",{number:jp,Complex:function(e){if(0===e.im)return p(e.re);e=new l(e.re-1,e.im);for(var t=new l(Ap[0],0),r=1;r<Ap.length;++r){var n=e.re+r,i=n*n+e.im*e.im;0!=i?(t.re+=Ap[r]*n/i,t.im+=-Ap[r]*e.im/i):t.re=Ap[r]<0?-1/0:1/0}var a=new l(e.re+Sp+.5,e.im),o=Math.sqrt(2*Math.PI);e.re+=.5;var s=f(a,e);0===s.im?s.re*=o:(0===s.re||(s.re*=o),s.im*=o);var u=Math.exp(-a.re);return a.re=u*Math.cos(-a.im),a.im=u*Math.sin(-a.im),c(c(s,a),t)},BigNumber:function(e){if(e.isInteger())return e.isNegative()||e.isZero()?new a(1/0):function(e){if(e.isZero())return new a(1);var t=i.precision+(0|Math.log(e.toNumber())),r=new(a.clone({precision:t}))(e),n=e.toNumber()-1;for(;1<n;)r=r.times(n),n--;return new a(r.toPrecision(a.precision))}(e.minus(1));if(!e.isFinite())return new a(e.isNegative()?NaN:1/0);throw new Error("Integer BigNumber expected")},"Array | Matrix":function(e){return oe(e,p)}});return p}),_p="factorial",Ip=["typed","gamma"],qp=Object(s.a)(_p,Ip,function(e){var t=e.typed,r=e.gamma,n=t(_p,{number:function(e){if(e<0)throw new Error("Value must be non-negative");return r(e+1)},BigNumber:function(e){if(e.isNegative())throw new Error("Value must be non-negative");return r(e.plus(1))},"Array | Matrix":function(e){return oe(e,n)}});return n}),Bp="kldivergence",kp=["typed","matrix","divide","sum","multiply","dotDivide","log","isNumeric"],zp=Object(s.a)(Bp,kp,function(e){var t=e.typed,r=e.matrix,s=e.divide,u=e.sum,c=e.multiply,f=e.dotDivide,l=e.log,p=e.isNumeric;return t(Bp,{"Array, Array":function(e,t){return n(r(e),r(t))},"Matrix, Array":function(e,t){return n(e,r(t))},"Array, Matrix":function(e,t){return n(r(e),t)},"Matrix, Matrix":function(e,t){return n(e,t)}});function n(e,t){var r=t.size().length,n=e.size().length;if(1<r)throw new Error("first object must be one dimensional");if(1<n)throw new Error("second object must be one dimensional");if(r!==n)throw new Error("Length of two vectors must be equal");if(0===u(e))throw new Error("Sum of elements in first object must be non zero");if(0===u(t))throw new Error("Sum of elements in second object must be non zero");var i=s(e,u(e)),a=s(t,u(t)),o=u(c(i,l(f(i,a))));return p(o)?o:Number.NaN}}),Dp="multinomial",Rp=["typed","add","divide","multiply","factorial","isInteger","isPositive"],Pp=Object(s.a)(Dp,Rp,function(e){var t=e.typed,n=e.add,i=e.divide,a=e.multiply,o=e.factorial,s=e.isInteger,u=e.isPositive;return t(Dp,{"Array | Matrix":function(e){var t=0,r=1;return F(e,function(e){if(!s(e)||!u(e))throw new TypeError("Positive integer value expected in function multinomial");t=n(t,e),r=a(r,o(e))}),i(o(t),r)}})}),Fp="permutations",Up=["typed","factorial"],Lp=Object(s.a)(Fp,Up,function(e){var t=e.typed,r=e.factorial;return t(Fp,{"number | BigNumber":r,"number, number":function(e,t){if(!Object(j.i)(e)||e<0)throw new TypeError("Positive integer value expected in function permutations");if(!Object(j.i)(t)||t<0)throw new TypeError("Positive integer value expected in function permutations");if(e<t)throw new TypeError("second argument k must be less than or equal to first argument n");return yp(e-t+1,e)},"BigNumber, BigNumber":function(e,t){var r,n;if(!Hp(e)||!Hp(t))throw new TypeError("Positive integer value expected in function permutations");if(t.gt(e))throw new TypeError("second argument k must be less than or equal to first argument n");for(r=e.mul(0).add(1),n=e.minus(t).plus(1);n.lte(e);n=n.plus(1))r=r.times(n);return r}})});function Hp(e){return e.isInteger()&&e.gte(0)}var $p=r(15),Gp=r.n($p),Zp=Gp()();function Vp(e){var t,r;return t=null===(r=e)?Zp:Gp()(String(r)),function(){return t()}}var Jp=["typed","config","?on"],Wp=Object(s.a)("pickRandom",Jp,function(e){var t=e.typed,r=e.config,n=e.on,m=Vp(r.randomSeed);return n&&n("config",function(e,t){e.randomSeed!==t.randomSeed&&(m=Vp(e.randomSeed))}),t({"Array | Matrix":function(e){return i(e)},"Array | Matrix, number":function(e,t){return i(e,t,void 0)},"Array | Matrix, Array":function(e,t){return i(e,void 0,t)},"Array | Matrix, Array | Matrix, number":function(e,t,r){return i(e,r,t)},"Array | Matrix, number, Array | Matrix":function(e,t,r){return i(e,t,r)}});function i(e,t,r){var n=void 0===t;if(n&&(t=1),e=e.valueOf(),r=r&&r.valueOf(),1<Object(I.a)(e).length)throw new Error("Only one dimensional vectors supported");var i=0;if(void 0!==r){if(r.length!==e.length)throw new Error("Weights must have the same length as possibles");for(var a=0,o=r.length;a<o;a++){if(!Object(ie.y)(r[a])||r[a]<0)throw new Error("Weights must be an array of positive numbers");i+=r[a]}}var s=e.length;if(0===s)return[];if(s<=t)return 1<t?e:e[0];for(var u,c=[];c.length<t;){if(void 0===r)u=e[Math.floor(m()*s)];else for(var f=m()*i,l=0,p=e.length;l<p;l++)if((f-=r[l])<0){u=e[l];break}-1===c.indexOf(u)&&c.push(u)}return n?c[0]:c}});function Yp(e,t){var r=[];if(1<(e=e.slice(0)).length)for(var n=0,i=e.shift();n<i;n++)r.push(Yp(e,t));else for(var a=0,o=e.shift();a<o;a++)r.push(t());return r}var Xp="random",Qp=["typed","config","?on"],Kp=Object(s.a)(Xp,Qp,function(e){var t=e.typed,r=e.config,n=e.on,i=Vp(r.randomSeed);return n&&n("config",function(e,t){e.randomSeed!==t.randomSeed&&(i=Vp(e.randomSeed))}),t(Xp,{"":function(){return o(0,1)},number:function(e){return o(0,e)},"number, number":function(e,t){return o(e,t)},"Array | Matrix":function(e){return a(e,0,1)},"Array | Matrix, number":function(e,t){return a(e,0,t)},"Array | Matrix, number, number":function(e,t,r){return a(e,t,r)}});function a(e,t,r){var n=Yp(e.valueOf(),function(){return o(t,r)});return Object(ie.v)(e)?e.create(n):n}function o(e,t){return e+i()*(t-e)}}),em="randomInt",tm=["typed","config","?on"],rm=Object(s.a)(em,tm,function(e){var t=e.typed,r=e.config,n=e.on,i=Vp(r.randomSeed);return n&&n("config",function(e,t){e.randomSeed!==t.randomSeed&&(i=Vp(e.randomSeed))}),t(em,{"":function(){return o(0,1)},number:function(e){return o(0,e)},"number, number":function(e,t){return o(e,t)},"Array | Matrix":function(e){return a(e,0,1)},"Array | Matrix, number":function(e,t){return a(e,0,t)},"Array | Matrix, number, number":function(e,t,r){return a(e,t,r)}});function a(e,t,r){var n=Yp(e.valueOf(),function(){return o(t,r)});return Object(ie.v)(e)?e.create(n):n}function o(e,t){return Math.floor(e+i()*(t-e))}}),nm="stirlingS2",im=["typed","addScalar","subtract","multiplyScalar","divideScalar","pow","factorial","combinations","isNegative","isInteger","larger"],am=Object(s.a)(nm,im,function(e){var t=e.typed,u=e.addScalar,c=e.subtract,f=e.multiplyScalar,l=e.divideScalar,p=e.pow,m=e.factorial,h=e.combinations,d=e.isNegative,y=e.isInteger,g=e.larger;return t(nm,{"number | BigNumber, number | BigNumber":function(e,t){if(!y(e)||d(e)||!y(t)||d(t))throw new TypeError("Non-negative integer value expected in function stirlingS2");if(g(t,e))throw new TypeError("k must be less than or equal to n in function stirlingS2");for(var r=m(t),n=0,i=0;i<=t;i++){var a=p(-1,c(t,i)),o=h(t,i),s=p(i,e);n=u(n,f(f(o,s),a))}return l(n,r)}})}),om="bellNumbers",sm=["typed","addScalar","isNegative","isInteger","stirlingS2"],um=Object(s.a)(om,sm,function(e){var t=e.typed,n=e.addScalar,i=e.isNegative,a=e.isInteger,o=e.stirlingS2;return t(om,{"number | BigNumber":function(e){if(!a(e)||i(e))throw new TypeError("Non-negative integer value expected in function bellNumbers");for(var t=0,r=0;r<=e;r++)t=n(t,o(e,r));return t}})}),cm="catalan",fm=["typed","addScalar","divideScalar","multiplyScalar","combinations","isNegative","isInteger"],lm=Object(s.a)(cm,fm,function(e){var t=e.typed,r=e.addScalar,n=e.divideScalar,i=e.multiplyScalar,a=e.combinations,o=e.isNegative,s=e.isInteger;return t(cm,{"number | BigNumber":function(e){if(!s(e)||o(e))throw new TypeError("Non-negative integer value expected in function catalan");return n(a(i(e,2),e),r(e,1))}})}),pm="composition",mm=["typed","addScalar","combinations","isNegative","isPositive","isInteger","larger"],hm=Object(s.a)(pm,mm,function(e){var t=e.typed,r=e.addScalar,n=e.combinations,i=e.isPositive,a=(e.isNegative,e.isInteger),o=e.larger;return t(pm,{"number | BigNumber, number | BigNumber":function(e,t){if(!(a(e)&&i(e)&&a(t)&&i(t)))throw new TypeError("Positive integer value expected in function composition");if(o(t,e))throw new TypeError("k must be less than or equal to n in function composition");return n(r(e,-1),r(t,-1))}})}),dm=["FunctionNode","OperatorNode","SymbolNode"],ym=Object(s.a)("simplifyUtil",dm,function(e){var r=e.FunctionNode,n=e.OperatorNode,i=e.SymbolNode,a={add:!0,multiply:!0},o={add:!0,multiply:!0};function s(e,t){if(!Object(ie.B)(e))return!1;var r=e.fn.toString();return t&&Object(ae.f)(t,r)&&Object(ae.f)(t[r],"associative")?t[r].associative:o[r]||!1}function u(e){var i,a=[];return s(e)?(i=e.op,function e(t){for(var r=0;r<t.args.length;r++){var n=t.args[r];Object(ie.B)(n)&&i===n.op?e(n):a.push(n)}}(e),a):e.args}function c(t){return Object(ie.B)(t)?function(e){try{return new n(t.op,t.fn,e,t.implicit)}catch(e){return console.error(e),[]}}:function(e){return new r(new i(t.name),e)}}return{createMakeNodeFunction:c,isCommutative:function(e,t){if(!Object(ie.B)(e))return!0;var r=e.fn.toString();return t&&Object(ae.f)(t,r)&&Object(ae.f)(t[r],"commutative")?t[r].commutative:a[r]||!1},isAssociative:s,flatten:function e(t){if(!t.args||0===t.args.length)return t;t.args=u(t);for(var r=0;r<t.args.length;r++)e(t.args[r])},allChildren:u,unflattenr:function e(t){if(t.args&&0!==t.args.length){for(var r=c(t),n=t.args.length,i=0;i<n;i++)e(t.args[i]);if(2<n&&s(t)){for(var a=t.args.pop();0<t.args.length;)a=r([t.args.pop(),a]);t.args=a.args}}},unflattenl:function e(t){if(t.args&&0!==t.args.length){for(var r=c(t),n=t.args.length,i=0;i<n;i++)e(t.args[i]);if(2<n&&s(t)){for(var a=t.args.shift();0<t.args.length;)a=r([a,t.args.shift()]);t.args=a.args}}}}}),gm=["equal","isZero","add","subtract","multiply","divide","pow","ConstantNode","OperatorNode","FunctionNode","ParenthesisNode"],vm=Object(s.a)("simplifyCore",gm,function(e){var f=e.equal,l=e.isZero,p=e.add,m=e.subtract,h=e.multiply,d=e.divide,y=e.pow,g=e.ConstantNode,v=e.OperatorNode,b=e.FunctionNode,x=e.ParenthesisNode,w=new g(0),N=new g(1);return function e(t){if(Object(ie.B)(t)&&t.isUnary()){var r=e(t.args[0]);if("+"===t.op)return r;if("-"===t.op){if(Object(ie.B)(r)){if(r.isUnary()&&"-"===r.op)return r.args[0];if(r.isBinary()&&"subtract"===r.fn)return new v("-","subtract",[r.args[1],r.args[0]])}return new v(t.op,t.fn,[r])}}else if(Object(ie.B)(t)&&t.isBinary()){var n=e(t.args[0]),i=e(t.args[1]);if("+"===t.op){if(Object(ie.l)(n)){if(l(n.value))return i;if(Object(ie.l)(i))return new g(p(n.value,i.value))}return Object(ie.l)(i)&&l(i.value)?n:Object(ie.B)(i)&&i.isUnary()&&"-"===i.op?new v("-","subtract",[n,i.args[0]]):new v(t.op,t.fn,i?[n,i]:[n])}if("-"===t.op){if(Object(ie.l)(n)&&i){if(Object(ie.l)(i))return new g(m(n.value,i.value));if(l(n.value))return new v("-","unaryMinus",[i])}if("subtract"===t.fn)return Object(ie.l)(i)&&l(i.value)?n:Object(ie.B)(i)&&i.isUnary()&&"-"===i.op?e(new v("+","add",[n,i.args[0]])):new v(t.op,t.fn,[n,i])}else{if("*"===t.op){if(Object(ie.l)(n)){if(l(n.value))return w;if(f(n.value,1))return i;if(Object(ie.l)(i))return new g(h(n.value,i.value))}if(Object(ie.l)(i)){if(l(i.value))return w;if(f(i.value,1))return n;if(Object(ie.B)(n)&&n.isBinary()&&n.op===t.op){var a=n.args[0];if(Object(ie.l)(a)){var o=new g(h(a.value,i.value));return new v(t.op,t.fn,[o,n.args[1]],t.implicit)}}return new v(t.op,t.fn,[i,n],t.implicit)}return new v(t.op,t.fn,[n,i],t.implicit)}if("/"===t.op){if(Object(ie.l)(n)){if(l(n.value))return w;if(Object(ie.l)(i)&&(f(i.value,1)||f(i.value,2)||f(i.value,4)))return new g(d(n.value,i.value))}return new v(t.op,t.fn,[n,i])}if("^"===t.op){if(Object(ie.l)(i)){if(l(i.value))return N;if(f(i.value,1))return n;if(Object(ie.l)(n))return new g(y(n.value,i.value));if(Object(ie.B)(n)&&n.isBinary()&&"^"===n.op){var s=n.args[1];if(Object(ie.l)(s))return new v(t.op,t.fn,[n.args[0],new g(h(s.value,i.value))])}}return new v(t.op,t.fn,[n,i])}}}else{if(Object(ie.C)(t)){var u=e(t.content);return Object(ie.C)(u)||Object(ie.J)(u)||Object(ie.l)(u)?u:new x(u)}if(Object(ie.r)(t)){var c=t.args.map(e).map(function(e){return Object(ie.C)(e)?e.content:e});return new b(e(t.fn),c)}}return t}}),bm=["typed","config","mathWithTransform","?fraction","?bignumber","ConstantNode","OperatorNode","FunctionNode","SymbolNode"],xm=Object(s.a)("simplifyConstant",bm,function(e){var t=e.typed,r=e.config,p=e.mathWithTransform,n=e.fraction,i=e.bignumber,a=e.ConstantNode,o=e.OperatorNode,m=e.FunctionNode,s=e.SymbolNode,u=ym({FunctionNode:m,OperatorNode:o,SymbolNode:s}),h=u.isCommutative,d=u.isAssociative,y=u.allChildren,g=u.createMakeNodeFunction;function v(t,r,n){try{return x(p[t].apply(null,r),n)}catch(e){return r=r.map(function(e){return Object(ie.o)(e)?e.valueOf():e}),x(p[t].apply(null,r),n)}}var b=t({Fraction:function(e){var t,r=e.s*e.n;t=r<0?new o("-","unaryMinus",[new a(-r)]):new a(r);return 1!==e.d?new o("/","divide",[t,new a(e.d)]):t},number:function(e){return e<0?f(new a(-e)):new a(e)},BigNumber:function(e){return e<0?f(new a(-e)):new a(e)},Complex:function(e){throw new Error("Cannot convert Complex number to Node")}});function c(e,t){if(t&&!1!==t.exactFractions&&isFinite(e)&&n){var r=n(e);if(r.valueOf()===e)return r}return e}var x=t({"string, Object":function(e,t){return"BigNumber"===r.number?(void 0===i&&wi(),i(e)):"Fraction"===r.number?(void 0===n&&Ni(),n(e)):c(parseFloat(e),t)},"Fraction, Object":function(e,t){return e},"BigNumber, Object":function(e,t){return e},"number, Object":function(e,t){return c(e,t)},"Complex, Object":function(e,t){return 0!==e.im?e:c(e.re,t)}});function f(e){return new o("-","unaryMinus",[e])}function w(r,e,n,i){return e.reduce(function(e,t){if(Object(ie.w)(e)||Object(ie.w)(t))Object(ie.w)(e)?Object(ie.w)(t)||(t=b(t)):e=b(e);else{try{return v(r,[e,t],i)}catch(e){}e=b(e),t=b(t)}return n([e,t])})}return function(e,t){var r=function t(e,r){switch(e.type){case"SymbolNode":return e;case"ConstantNode":return"number"!=typeof e.value&&isNaN(e.value)?e:x(e.value,r);case"FunctionNode":if(p[e.name]&&p[e.name].rawArgs)return e;var n=["add","multiply"];if(-1===n.indexOf(e.name)){var i=e.args.map(function(e){return t(e,r)});if(!i.some(ie.w))try{return v(e.name,i,r)}catch(e){}return i=i.map(function(e){return Object(ie.w)(e)?e:b(e)}),new m(e.name,i)}case"OperatorNode":var a,o,s=e.fn.toString(),u=g(e);if(Object(ie.B)(e)&&e.isUnary())a=[t(e.args[0],r)],o=Object(ie.w)(a[0])?u(a):v(s,a,r);else if(d(e))if(a=(a=y(e)).map(function(e){return t(e,r)}),h(s)){for(var c=[],f=[],l=0;l<a.length;l++)Object(ie.w)(a[l])?f.push(a[l]):c.push(a[l]);o=1<c.length?(o=w(s,c,u,r),f.unshift(o),w(s,f,u,r)):w(s,a,u,r)}else o=w(s,a,u,r);else a=e.args.map(function(e){return t(e,r)}),o=w(s,a,u,r);return o;case"ParenthesisNode":return t(e.content,r);case"AccessorNode":case"ArrayNode":case"AssignmentNode":case"BlockNode":case"FunctionAssignmentNode":case"IndexNode":case"ObjectNode":case"RangeNode":case"ConditionalNode":default:throw new Error("Unimplemented node type in simplifyConstant: ".concat(e.type))}}(e,t);return Object(ie.w)(r)?r:b(r)}}),wm=["parse","FunctionNode","OperatorNode","ParenthesisNode"],Nm=Object(s.a)("resolve",wm,function(e){var o=e.parse,s=e.FunctionNode,u=e.OperatorNode,c=e.ParenthesisNode;return function t(e,r){if(!r)return e;if(Object(ie.J)(e)){var n=r[e.name];if(Object(ie.w)(n))return t(n,r);if("number"==typeof n)return o(String(n))}else{if(Object(ie.B)(e)){var i=e.args.map(function(e){return t(e,r)});return new u(e.op,e.fn,i,e.implicit)}if(Object(ie.C)(e))return new c(t(e.content,r));if(Object(ie.r)(e)){var a=e.args.map(function(e){return t(e,r)});return new s(e.name,a)}}return e}});function Om(e){return(Om="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var Mm=["config","typed","parse","add","subtract","multiply","divide","pow","isZero","equal","?fraction","?bignumber","mathWithTransform","ConstantNode","FunctionNode","OperatorNode","ParenthesisNode","SymbolNode"],Em=Object(s.a)("simplify",Mm,function(e){var t=e.config,r=e.typed,c=e.parse,n=e.add,i=e.subtract,a=e.multiply,o=e.divide,s=e.pow,u=e.isZero,l=e.equal,f=e.fraction,p=e.bignumber,m=e.mathWithTransform,h=e.ConstantNode,d=e.FunctionNode,y=e.OperatorNode,g=e.ParenthesisNode,v=e.SymbolNode,b=xm({typed:r,config:t,mathWithTransform:m,fraction:f,bignumber:p,ConstantNode:h,OperatorNode:y,FunctionNode:d,SymbolNode:v}),x=vm({equal:l,isZero:u,add:n,subtract:i,multiply:a,divide:o,pow:s,ConstantNode:h,OperatorNode:y,FunctionNode:d,ParenthesisNode:g}),w=Nm({parse:c,FunctionNode:d,OperatorNode:y,ParenthesisNode:g}),N=ym({FunctionNode:d,OperatorNode:y,SymbolNode:v}),O=N.isCommutative,M=N.isAssociative,E=N.flatten,j=N.unflattenr,S=N.unflattenl,A=N.createMakeNodeFunction,C=r("simplify",{string:function(e){return C(c(e),C.rules,{},{})},"string, Object":function(e,t){return C(c(e),C.rules,t,{})},"string, Object, Object":function(e,t,r){return C(c(e),C.rules,t,r)},"string, Array":function(e,t){return C(c(e),t,{},{})},"string, Array, Object":function(e,t,r){return C(c(e),t,r,{})},"string, Array, Object, Object":function(e,t,r,n){return C(c(e),t,r,n)},"Node, Object":function(e,t){return C(e,C.rules,t,{})},"Node, Object, Object":function(e,t,r){return C(e,C.rules,t,r)},Node:function(e){return C(e,C.rules,{},{})},"Node, Array":function(e,t){return C(e,t,{},{})},"Node, Array, Object":function(e,t,r){return C(e,t,r,{})},"Node, Array, Object, Object":function(e,t,r,n){t=function(e){for(var t=[],r=0;r<e.length;r++){var n=e[r],i=void 0,a=Om(n);switch(a){case"string":var o=n.split("->");if(2!==o.length)throw SyntaxError("Could not parse rule: "+n);n={l:o[0],r:o[1]};case"object":if(i={l:T(c(n.l)),r:T(c(n.r))},n.context&&(i.evaluate=n.context),n.evaluate&&(i.evaluate=c(n.evaluate)),M(i.l)){var s=A(i.l),u=new v("_p"+I++);i.expanded={},i.expanded.l=s([i.l.clone(),u]),E(i.expanded.l),j(i.expanded.l),i.expanded.r=s([i.r,u])}break;case"function":i=n;break;default:throw TypeError("Unsupported type of rule: "+a)}t.push(i)}return t}(t);for(var i=w(e,r),a={},o=(i=T(i)).toString({parenthesis:"all"});!a[o];){a[o]=!0;for(var s=I=0;s<t.length;s++)i="function"==typeof t[s]?t[s](i,n):(E(i),q(i,t[s])),S(i);o=i.toString({parenthesis:"all"})}return i}});function T(e){return e.transform(function(e,t,r){return Object(ie.C)(e)?T(e.content):e})}C.simplifyCore=x,C.resolve=w;var _={true:!0,false:!0,e:!0,i:!0,Infinity:!0,LN2:!0,LN10:!0,LOG2E:!0,LOG10E:!0,NaN:!0,phi:!0,pi:!0,SQRT1_2:!0,SQRT2:!0,tau:!0};C.rules=[x,{l:"log(e)",r:"1"},{l:"n-n1",r:"n+-n1"},{l:"-(c*v)",r:"(-c) * v"},{l:"-v",r:"(-1) * v"},{l:"n/n1^n2",r:"n*n1^-n2"},{l:"n/n1",r:"n*n1^-1"},{l:"(n ^ n1) ^ n2",r:"n ^ (n1 * n2)"},{l:"n*n",r:"n^2"},{l:"n * n^n1",r:"n^(n1+1)"},{l:"n^n1 * n^n2",r:"n^(n1+n2)"},{l:"n+n",r:"2*n"},{l:"n+-n",r:"0"},{l:"n1*n2 + n2",r:"(n1+1)*n2"},{l:"n1*n3 + n2*n3",r:"(n1+n2)*n3"},{l:"n1 + -1 * (n2 + n3)",r:"n1 + -1 * n2 + -1 * n3"},b,{l:"(-n)*n1",r:"-(n*n1)"},{l:"c+v",r:"v+c",context:{add:{commutative:!1}}},{l:"v*c",r:"c*v",context:{multiply:{commutative:!1}}},{l:"n+-n1",r:"n-n1"},{l:"n*(n1^-1)",r:"n/n1"},{l:"n*n1^-n2",r:"n/n1^n2"},{l:"n1^-1",r:"1/n1"},{l:"n*(n1/n2)",r:"(n*n1)/n2"},{l:"n-(n1+n2)",r:"n-n1-n2"},{l:"1*n",r:"n"}];var I=0;var q=r("applyRule",{"Node, Object":function(e,t){var r=e;if(r instanceof y||r instanceof d){if(r.args)for(var n=0;n<r.args.length;n++)r.args[n]=q(r.args[n],t)}else r instanceof g&&r.content&&(r.content=q(r.content,t));var i=t.r,a=z(t.l,r)[0];if(!a&&t.expanded&&(i=t.expanded.r,a=z(t.expanded.l,r)[0]),a){var o=r.implicit;r=i.clone(),o&&"implicit"in i&&(r.implicit=!0),r=r.transform(function(e){return e.isSymbolNode&&Object(ae.f)(a.placeholders,e.name)?a.placeholders[e.name].clone():e})}return r}});function B(e,t){var r={placeholders:{}};if(!e.placeholders&&!t.placeholders)return r;if(!e.placeholders)return t;if(!t.placeholders)return e;for(var n in e.placeholders)if(r.placeholders[n]=e.placeholders[n],Object(ae.f)(t.placeholders,n)&&!D(e.placeholders[n],t.placeholders[n]))return null;for(var i in t.placeholders)r.placeholders[i]=t.placeholders[i];return r}function k(e,t){var r,n=[];if(0===e.length||0===t.length)return n;for(var i=0;i<e.length;i++)for(var a=0;a<t.length;a++)(r=B(e[i],t[a]))&&n.push(r);return n}function z(e,t,r){var n=[{placeholders:{}}];if(e instanceof y&&t instanceof y||e instanceof d&&t instanceof d){if(e instanceof y){if(e.op!==t.op||e.fn!==t.fn)return[]}else if(e instanceof d&&e.name!==t.name)return[];if((1!==t.args.length||1!==e.args.length)&&M(t)&&!r){if(2<=t.args.length&&2===e.args.length){for(var i=function(e,t){var r,n,i=[],a=A(e);if(O(e,t))for(var o=0;o<e.args.length;o++)(n=e.args.slice(0)).splice(o,1),r=1===n.length?n[0]:a(n),i.push(a([e.args[o],r]));else r=1===(n=e.args.slice(1)).length?n[0]:a(n),i.push(a([e.args[0],r]));return i}(t,e.context),a=[],o=0;o<i.length;o++){var s=z(e,i[o],!0);a=a.concat(s)}return a}if(2<e.args.length)throw Error("Unexpected non-binary associative function: "+e.toString());return[]}for(var u=[],c=0;c<e.args.length;c++){var f=z(e.args[c],t.args[c]);if(0===f.length)return[];u.push(f)}n=function(e){if(0===e.length)return e;for(var t=e.reduce(k),r=[],n={},i=0;i<t.length;i++){var a=JSON.stringify(t[i]);n[a]||(n[a]=!0,r.push(t[i]))}return r}(u)}else if(e instanceof v){if(0===e.name.length)throw new Error("Symbol in rule has 0 length...!?");if(_[e.name]){if(e.name!==t.name)return[]}else if("n"===e.name[0]||"_p"===e.name.substring(0,2))n[0].placeholders[e.name]=t;else if("v"===e.name[0]){if(Object(ie.l)(t))return[];n[0].placeholders[e.name]=t}else{if("c"!==e.name[0])throw new Error("Invalid symbol in rule: "+e.name);if(!(t instanceof h))return[];n[0].placeholders[e.name]=t}}else{if(!(e instanceof h))return[];if(!l(e.value,t.value))return[]}return n}function D(e,t){if(e instanceof h&&t instanceof h){if(!l(e.value,t.value))return!1}else if(e instanceof v&&t instanceof v){if(e.name!==t.name)return!1}else{if(!(e instanceof y&&t instanceof y||e instanceof d&&t instanceof d))return!1;if(e instanceof y){if(e.op!==t.op||e.fn!==t.fn)return!1}else if(e instanceof d&&e.name!==t.name)return!1;if(e.args.length!==t.args.length)return!1;for(var r=0;r<e.args.length;r++)if(!D(e.args[r],t.args[r]))return!1}return!0}return C}),jm=["typed","config","parse","simplify","equal","isZero","numeric","ConstantNode","FunctionNode","OperatorNode","ParenthesisNode","SymbolNode"],Sm=Object(s.a)("derivative",jm,function(e){var t=e.typed,r=e.config,n=e.parse,a=e.simplify,l=e.equal,p=e.isZero,i=e.numeric,o=e.ConstantNode,m=e.FunctionNode,h=e.OperatorNode,s=e.ParenthesisNode,f=e.SymbolNode,u=t("derivative",{"Node, SymbolNode, Object":function(e,t,r){var n={};d(n,e,t.name);var i=y(e,n);return r.simplify?a(i):i},"Node, SymbolNode":function(e,t){return u(e,t,{simplify:!0})},"string, SymbolNode":function(e,t){return u(n(e),t)},"string, SymbolNode, Object":function(e,t,r){return u(n(e),t,r)},"string, string":function(e,t){return u(n(e),n(t))},"string, string, Object":function(e,t,r){return u(n(e),n(t),r)},"Node, string":function(e,t){return u(e,n(t))},"Node, string, Object":function(e,t,r){return u(e,n(t),r)}});u._simplify=!0,u.toTex=function(e){return c.apply(null,e.args)};var c=t("_derivTex",{"Node, SymbolNode":function(e,t){return Object(ie.l)(e)&&"string"===Object(ie.M)(e.value)?c(n(e.value).toString(),t.toString(),1):c(e.toString(),t.toString(),1)},"Node, ConstantNode":function(e,t){if("string"===Object(ie.M)(t.value))return c(e,n(t.value));throw new Error("The second parameter to 'derivative' is a non-string constant")},"Node, SymbolNode, ConstantNode":function(e,t,r){return c(e.toString(),t.name,r.value)},"string, string, number":function(e,t,r){return(1===r?"{d\\over d"+t+"}":"{d^{"+r+"}\\over d"+t+"^{"+r+"}}")+"\\left[".concat(e,"\\right]")}}),d=t("constTag",{"Object, ConstantNode, string":function(e,t){return e[t]=!0},"Object, SymbolNode, string":function(e,t,r){return t.name!==r&&(e[t]=!0)},"Object, ParenthesisNode, string":function(e,t,r){return d(e,t.content,r)},"Object, FunctionAssignmentNode, string":function(e,t,r){return-1===t.params.indexOf(r)?e[t]=!0:d(e,t.expr,r)},"Object, FunctionNode | OperatorNode, string":function(e,t,r){if(0<t.args.length){for(var n=d(e,t.args[0],r),i=1;i<t.args.length;++i)n=d(e,t.args[i],r)&&n;if(n)return e[t]=!0}return!1}}),y=t("_derivative",{"ConstantNode, Object":function(e){return g(0)},"SymbolNode, Object":function(e,t){return void 0!==t[e]?g(0):g(1)},"ParenthesisNode, Object":function(e,t){return new s(y(e.content,t))},"FunctionAssignmentNode, Object":function(e,t){return void 0!==t[e]?g(0):y(e.expr,t)},"FunctionNode, Object":function(e,t){if(1!==e.args.length&&function(e){if(("log"===e.name||"nthRoot"===e.name||"pow"===e.name)&&2===e.args.length)return;for(var t=0;t<e.args.length;++t)e.args[t]=g(0);throw e.compile().evaluate(),new Error("Expected TypeError, but none found")}(e),void 0!==t[e])return g(0);var r,n,i,a,o=e.args[0],s=!1,u=!1;switch(e.name){case"cbrt":s=!0,n=new h("*","multiply",[g(3),new h("^","pow",[o,new h("/","divide",[g(2),g(3)])])]);break;case"sqrt":case"nthRoot":if(1===e.args.length)s=!0,n=new h("*","multiply",[g(2),new m("sqrt",[o])]);else if(2===e.args.length)return t[r=new h("/","divide",[g(1),e.args[1]])]=t[e.args[1]],y(new h("^","pow",[o,r]),t);break;case"log10":r=g(10);case"log":if(r||1!==e.args.length){if(1===e.args.length&&r||2===e.args.length&&void 0!==t[e.args[1]])n=new h("*","multiply",[o.clone(),new m("log",[r||e.args[1]])]),s=!0;else if(2===e.args.length)return y(new h("/","divide",[new m("log",[o]),new m("log",[e.args[1]])]),t)}else n=o.clone(),s=!0;break;case"pow":return t[r]=t[e.args[1]],y(new h("^","pow",[o,e.args[1]]),t);case"exp":n=new m("exp",[o.clone()]);break;case"sin":n=new m("cos",[o.clone()]);break;case"cos":n=new h("-","unaryMinus",[new m("sin",[o.clone()])]);break;case"tan":n=new h("^","pow",[new m("sec",[o.clone()]),g(2)]);break;case"sec":n=new h("*","multiply",[e,new m("tan",[o.clone()])]);break;case"csc":u=!0,n=new h("*","multiply",[e,new m("cot",[o.clone()])]);break;case"cot":u=!0,n=new h("^","pow",[new m("csc",[o.clone()]),g(2)]);break;case"asin":s=!0,n=new m("sqrt",[new h("-","subtract",[g(1),new h("^","pow",[o.clone(),g(2)])])]);break;case"acos":u=s=!0,n=new m("sqrt",[new h("-","subtract",[g(1),new h("^","pow",[o.clone(),g(2)])])]);break;case"atan":s=!0,n=new h("+","add",[new h("^","pow",[o.clone(),g(2)]),g(1)]);break;case"asec":s=!0,n=new h("*","multiply",[new m("abs",[o.clone()]),new m("sqrt",[new h("-","subtract",[new h("^","pow",[o.clone(),g(2)]),g(1)])])]);break;case"acsc":u=s=!0,n=new h("*","multiply",[new m("abs",[o.clone()]),new m("sqrt",[new h("-","subtract",[new h("^","pow",[o.clone(),g(2)]),g(1)])])]);break;case"acot":u=s=!0,n=new h("+","add",[new h("^","pow",[o.clone(),g(2)]),g(1)]);break;case"sinh":n=new m("cosh",[o.clone()]);break;case"cosh":n=new m("sinh",[o.clone()]);break;case"tanh":n=new h("^","pow",[new m("sech",[o.clone()]),g(2)]);break;case"sech":u=!0,n=new h("*","multiply",[e,new m("tanh",[o.clone()])]);break;case"csch":u=!0,n=new h("*","multiply",[e,new m("coth",[o.clone()])]);break;case"coth":u=!0,n=new h("^","pow",[new m("csch",[o.clone()]),g(2)]);break;case"asinh":s=!0,n=new m("sqrt",[new h("+","add",[new h("^","pow",[o.clone(),g(2)]),g(1)])]);break;case"acosh":s=!0,n=new m("sqrt",[new h("-","subtract",[new h("^","pow",[o.clone(),g(2)]),g(1)])]);break;case"atanh":s=!0,n=new h("-","subtract",[g(1),new h("^","pow",[o.clone(),g(2)])]);break;case"asech":u=s=!0,n=new h("*","multiply",[o.clone(),new m("sqrt",[new h("-","subtract",[g(1),new h("^","pow",[o.clone(),g(2)])])])]);break;case"acsch":u=s=!0,n=new h("*","multiply",[new m("abs",[o.clone()]),new m("sqrt",[new h("+","add",[new h("^","pow",[o.clone(),g(2)]),g(1)])])]);break;case"acoth":u=s=!0,n=new h("-","subtract",[g(1),new h("^","pow",[o.clone(),g(2)])]);break;case"abs":n=new h("/","divide",[new m(new f("abs"),[o.clone()]),o.clone()]);break;case"gamma":default:throw new Error('Function "'+e.name+'" is not supported by derivative, or a wrong number of arguments is passed')}a=s?(i="/","divide"):(i="*","multiply");var c=y(o,t);return u&&(c=new h("-","unaryMinus",[c])),new h(i,a,[c,n])},"OperatorNode, Object":function(e,r){if(void 0!==r[e])return g(0);if("+"===e.op)return new h(e.op,e.fn,e.args.map(function(e){return y(e,r)}));if("-"===e.op){if(e.isUnary())return new h(e.op,e.fn,[y(e.args[0],r)]);if(e.isBinary())return new h(e.op,e.fn,[y(e.args[0],r),y(e.args[1],r)])}if("*"===e.op){var t=e.args.filter(function(e){return void 0!==r[e]});if(0<t.length){var n=e.args.filter(function(e){return void 0===r[e]}),i=1===n.length?n[0]:new h("*","multiply",n),a=t.concat(y(i,r));return new h("*","multiply",a)}return new h("+","add",e.args.map(function(t){return new h("*","multiply",e.args.map(function(e){return e===t?y(e,r):e.clone()}))}))}if("/"===e.op&&e.isBinary()){var o=e.args[0],s=e.args[1];return void 0!==r[s]?new h("/","divide",[y(o,r),s]):void 0!==r[o]?new h("*","multiply",[new h("-","unaryMinus",[o]),new h("/","divide",[y(s,r),new h("^","pow",[s.clone(),g(2)])])]):new h("/","divide",[new h("-","subtract",[new h("*","multiply",[y(o,r),s.clone()]),new h("*","multiply",[o.clone(),y(s,r)])]),new h("^","pow",[s.clone(),g(2)])])}if("^"===e.op&&e.isBinary()){var u=e.args[0],c=e.args[1];if(void 0!==r[u])return Object(ie.l)(u)&&(p(u.value)||l(u.value,1))?g(0):new h("*","multiply",[e,new h("*","multiply",[new m("log",[u.clone()]),y(c.clone(),r)])]);if(void 0===r[c])return new h("*","multiply",[new h("^","pow",[u.clone(),c.clone()]),new h("+","add",[new h("*","multiply",[y(u,r),new h("/","divide",[c.clone(),u.clone()])]),new h("*","multiply",[y(c,r),new m("log",[u.clone()])])])]);if(Object(ie.l)(c)){if(p(c.value))return g(0);if(l(c.value,1))return y(u,r)}var f=new h("^","pow",[u.clone(),new h("-","subtract",[c,g(1)])]);return new h("*","multiply",[c.clone(),new h("*","multiply",[y(u,r),f])])}throw new Error('Operator "'+e.op+'" is not supported by derivative, or a wrong number of arguments is passed')}});function g(e,t){return new o(i(e,t||r.number))}return u}),Am="rationalize",Cm=["config","typed","equal","isZero","add","subtract","multiply","divide","pow","parse","simplify","?bignumber","?fraction","mathWithTransform","ConstantNode","OperatorNode","FunctionNode","SymbolNode","ParenthesisNode"],Tm=Object(s.a)(Am,Cm,function(e){var t=e.config,r=e.typed,n=e.equal,i=e.isZero,a=e.add,o=e.subtract,s=e.multiply,u=e.divide,c=e.pow,f=e.parse,m=e.simplify,l=e.fraction,p=e.bignumber,h=e.mathWithTransform,d=e.ConstantNode,y=e.OperatorNode,g=e.FunctionNode,v=e.SymbolNode,b=e.ParenthesisNode,x=xm({typed:r,config:t,mathWithTransform:h,fraction:l,bignumber:p,ConstantNode:d,OperatorNode:y,FunctionNode:g,SymbolNode:v}),w=vm({equal:n,isZero:i,add:a,subtract:o,multiply:s,divide:u,pow:c,ConstantNode:d,OperatorNode:y,FunctionNode:g,ParenthesisNode:b}),N=r(Am,{string:function(e){return N(f(e),{},!1)},"string, boolean":function(e,t){return N(f(e),{},t)},"string, Object":function(e,t){return N(f(e),t,!1)},"string, Object, boolean":function(e,t,r){return N(f(e),t,r)},Node:function(e){return N(e,{},!1)},"Node, boolean":function(e,t){return N(e,{},t)},"Node, Object":function(e,t){return N(e,t,!1)},"Node, Object, boolean":function(e,t,r){var n=function(){var e=[w,{l:"n+n",r:"2*n"},{l:"n+-n",r:"0"},x,{l:"n*(n1^-1)",r:"n/n1"},{l:"n*n1^-n2",r:"n/n1^n2"},{l:"n1^-1",r:"1/n1"},{l:"n*(n1/n2)",r:"(n*n1)/n2"},{l:"1*n",r:"n"}],t=[{l:"(-n1)/(-n2)",r:"n1/n2"},{l:"(-n1)*(-n2)",r:"n1*n2"},{l:"n1--n2",r:"n1+n2"},{l:"n1-n2",r:"n1+(-n2)"},{l:"(n1+n2)*n3",r:"(n1*n3 + n2*n3)"},{l:"n1*(n2+n3)",r:"(n1*n2+n1*n3)"},{l:"c1*n + c2*n",r:"(c1+c2)*n"},{l:"c1*n + n",r:"(c1+1)*n"},{l:"c1*n - c2*n",r:"(c1-c2)*n"},{l:"c1*n - n",r:"(c1-1)*n"},{l:"v/c",r:"(1/c)*v"},{l:"v/-c",r:"-(1/c)*v"},{l:"-v*-c",r:"c*v"},{l:"-v*c",r:"-c*v"},{l:"v*-c",r:"-c*v"},{l:"v*c",r:"c*v"},{l:"-(-n1*n2)",r:"(n1*n2)"},{l:"-(n1*n2)",r:"(-n1*n2)"},{l:"-(-n1+n2)",r:"(n1-n2)"},{l:"-(n1+n2)",r:"(-n1-n2)"},{l:"(n1^n2)^n3",r:"(n1^(n2*n3))"},{l:"-(-n1/n2)",r:"(n1/n2)"},{l:"-(n1/n2)",r:"(-n1/n2)"}],r=[{l:"(n1/(n2/n3))",r:"((n1*n3)/n2)"},{l:"(n1/n2/n3)",r:"(n1/(n2*n3))"}],n={};return n.firstRules=e.concat(t,r),n.distrDivRules=[{l:"(n1/n2 + n3/n4)",r:"((n1*n4 + n3*n2)/(n2*n4))"},{l:"(n1/n2 + n3)",r:"((n1 + n3*n2)/n2)"},{l:"(n1 + n2/n3)",r:"((n1*n3 + n2)/n3)"}],n.sucDivRules=r,n.firstRulesAgain=e.concat(t),n.finalRules=[w,{l:"n*-n",r:"-n^2"},{l:"n*n",r:"n^2"},x,{l:"n*-n^n1",r:"-n^(n1+1)"},{l:"n*n^n1",r:"n^(n1+1)"},{l:"n^n1*-n^n2",r:"-n^(n1+n2)"},{l:"n^n1*n^n2",r:"n^(n1+n2)"},{l:"n^n1*-n",r:"-n^(n1+1)"},{l:"n^n1*n",r:"n^(n1+1)"},{l:"n^n1/-n",r:"-n^(n1-1)"},{l:"n^n1/n",r:"n^(n1-1)"},{l:"n/-n^n1",r:"-n^(1-n1)"},{l:"n/n^n1",r:"n^(1-n1)"},{l:"n^n1/-n^n2",r:"n^(n1-n2)"},{l:"n^n1/n^n2",r:"n^(n1-n2)"},{l:"n1+(-n2*n3)",r:"n1-n2*n3"},{l:"v*(-c)",r:"-c*v"},{l:"n1+-n2",r:"n1-n2"},{l:"v*c",r:"c*v"},{l:"(n1^n2)^n3",r:"(n1^(n2*n3))"}],n}(),i=function(e,t,r,n){var o=[],i=m(e,n,t,{exactFractions:!1}),s="+-*"+((r=!!r)?"/":"");!function e(t){var r=t.type;{if("FunctionNode"===r)throw new Error("There is an unsolved function call");if("OperatorNode"===r)if("^"===t.op){if("unaryMinus"===t.args[1].fn&&(t=t.args[0]),"ConstantNode"!==t.args[1].type||!Object(j.i)(parseFloat(t.args[1].value)))throw new Error("There is a non-integer exponent");e(t.args[0])}else{if(-1===s.indexOf(t.op))throw new Error("Operator "+t.op+" invalid in polynomial expression");for(var n=0;n<t.args.length;n++)e(t.args[n])}else if("SymbolNode"===r){var i=t.name,a=o.indexOf(i);-1===a&&o.push(i)}else if("ParenthesisNode"===r)e(t.content);else if("ConstantNode"!==r)throw new Error("type "+r+" is not allowed in polynomial expression")}}(i);var a={};return a.expression=i,a.variables=o,a}(e,t,!0,n.firstRules),a=i.variables.length;if(e=i.expression,1<=a){var o,s;e=function e(t,r,n){var i=t.type;var a=1<arguments.length;if("OperatorNode"===i&&t.isBinary()){var o,s=!1;if("^"===t.op&&("ParenthesisNode"!==t.args[0].type&&"OperatorNode"!==t.args[0].type||"ConstantNode"!==t.args[1].type||(o=parseFloat(t.args[1].value),s=2<=o&&Object(j.i)(o))),s){if(2<o){var u=t.args[0],c=new y("^","pow",[t.args[0].cloneDeep(),new d(o-1)]);t=new y("*","multiply",[u,c])}else t=new y("*","multiply",[t.args[0],t.args[0].cloneDeep()]);a&&("content"===n?r.content=t:r.args[n]=t)}}if("ParenthesisNode"===i)e(t.content,t,"content");else if("ConstantNode"!==i&&"SymbolNode"!==i)for(var f=0;f<t.args.length;f++)e(t.args[f],t,f);if(!a)return t}(e);var u,c=!0,f=!1;for(e=m(e,n.firstRules,{},{exactFractions:!1});s=c?n.distrDivRules:n.sucDivRules,c=!c,(u=(e=m(e,s)).toString())!==o;)f=!0,o=u;f&&(e=m(e,n.firstRulesAgain,{},{exactFractions:!1})),e=m(e,n.finalRules,{},{exactFractions:!1})}var l=[],p={};return"OperatorNode"===e.type&&e.isBinary()&&"/"===e.op?(1===a&&(e.args[0]=O(e.args[0],l),e.args[1]=O(e.args[1])),r&&(p.numerator=e.args[0],p.denominator=e.args[1])):(1===a&&(e=O(e,l)),r&&(p.numerator=e,p.denominator=null)),r?(p.coefficients=l,p.variables=i.variables,p.expression=e,p):e}});function O(e,u){void 0===u&&(u=[]);var t={cte:1,oper:"+",fire:""},c=u[0]=0,f="";!function e(t,r,n){var i=t.type;{if("FunctionNode"===i)throw new Error("There is an unsolved function call");if("OperatorNode"===i){if(-1==="+-*^".indexOf(t.op))throw new Error("Operator "+t.op+" invalid");if(null!==r){if(("unaryMinus"===t.fn||"pow"===t.fn)&&"add"!==r.fn&&"subtract"!==r.fn&&"multiply"!==r.fn)throw new Error("Invalid "+t.op+" placing");if(("subtract"===t.fn||"add"===t.fn||"multiply"===t.fn)&&"add"!==r.fn&&"subtract"!==r.fn)throw new Error("Invalid "+t.op+" placing");if(("subtract"===t.fn||"add"===t.fn||"unaryMinus"===t.fn)&&0!==n.noFil)throw new Error("Invalid "+t.op+" placing")}"^"!==t.op&&"*"!==t.op||(n.fire=t.op);for(var a=0;a<t.args.length;a++)"unaryMinus"===t.fn&&(n.oper="-"),"+"!==t.op&&"subtract"!==t.fn||(n.fire="",n.cte=1,n.oper=0===a?"+":t.op),n.noFil=a,e(t.args[a],t,n)}else if("SymbolNode"===i){if(t.name!==f&&""!==f)throw new Error("There is more than one variable");if(f=t.name,null===r)return void(u[1]=1);if("^"===r.op&&0!==n.noFil)throw new Error("In power the variable should be the first parameter");if("*"===r.op&&1!==n.noFil)throw new Error("In multiply the variable should be the second parameter");""!==n.fire&&"*"!==n.fire||(c<1&&(u[1]=0),u[1]+=n.cte*("+"===n.oper?1:-1),c=Math.max(1,c))}else{if("ConstantNode"!==i)throw new Error("Type "+i+" is not allowed");var o=parseFloat(t.value);if(null===r)return void(u[0]=o);if("^"===r.op){if(1!==n.noFil)throw new Error("Constant cannot be powered");if(!Object(j.i)(o)||o<=0)throw new Error("Non-integer exponent is not allowed");for(var s=c+1;s<o;s++)u[s]=0;return c<o&&(u[o]=0),u[o]+=n.cte*("+"===n.oper?1:-1),void(c=Math.max(o,c))}n.cte=o,""===n.fire&&(u[0]+=n.cte*("+"===n.oper?1:-1))}}}(e,null,t);for(var r,n=!0,i=c=u.length-1;0<=i;i--)if(0!==u[i]){var a=new d(n?u[i]:Math.abs(u[i])),o=u[i]<0?"-":"+";if(0<i){var s=new v(f);if(1<i){var l=new d(i);s=new y("^","pow",[s,l])}a=-1===u[i]&&n?new y("-","unaryMinus",[s]):1===Math.abs(u[i])?s:new y("*","multiply",[a,s])}r=n?a:"+"==o?new y("+","add",[r,a]):new y("-","subtract",[r,a]),n=!1}return n?new d(0):r}return N}),_m=["classes"],Im=Object(s.a)("reviver",_m,function(e){var n=e.classes;return function(e,t){var r=n[t&&t.mathjs];return r&&"function"==typeof r.fromJSON?r.fromJSON(t):t}}),qm=Math.PI,Bm=2*Math.PI,km=Math.E,zm=Object(s.a)("true",[],function(){return!0}),Dm=Object(s.a)("false",[],function(){return!1}),Rm=Object(s.a)("null",[],function(){return null}),Pm=th("Infinity",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(1/0):1/0}),Fm=th("NaN",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(NaN):NaN}),Um=th("pi",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?bs(r):qm}),Lm=th("tau",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?xs(r):Bm}),Hm=th("e",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?gs(r):km}),$m=th("phi",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?vs(r):1.618033988749895}),Gm=th("LN2",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(2).ln():Math.LN2}),Zm=th("LN10",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(10).ln():Math.LN10}),Vm=th("LOG2E",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(1).div(new r(2).ln()):Math.LOG2E}),Jm=th("LOG10E",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(1).div(new r(10).ln()):Math.LOG10E}),Wm=th("SQRT1_2",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r("0.5").sqrt():Math.SQRT1_2}),Ym=th("SQRT2",["config","?BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(2).sqrt():Math.SQRT2}),Xm=th("i",["Complex"],function(e){return e.Complex.I}),Qm=Object(s.a)("PI",["pi"],function(e){return e.pi}),Km=Object(s.a)("E",["e"],function(e){return e.e}),eh=Object(s.a)("version",[],function(){return"6.2.3"});function th(e,t,r){return Object(s.a)(e,t,r,{recreateOnConfigChange:!0})}var rh=Qh("speedOfLight","299792458","m s^-1"),nh=Qh("gravitationConstant","6.67430e-11","m^3 kg^-1 s^-2"),ih=Qh("planckConstant","6.62607015e-34","J s"),ah=Qh("reducedPlanckConstant","1.0545718176461565e-34","J s"),oh=Qh("magneticConstant","1.25663706212e-6","N A^-2"),sh=Qh("electricConstant","8.8541878128e-12","F m^-1"),uh=Qh("vacuumImpedance","376.730313667","ohm"),ch=Qh("coulomb","8.987551792261171e9","N m^2 C^-2"),fh=Qh("elementaryCharge","1.602176634e-19","C"),lh=Qh("bohrMagneton","9.2740100783e-24","J T^-1"),ph=Qh("conductanceQuantum","7.748091729863649e-5","S"),mh=Qh("inverseConductanceQuantum","12906.403729652257","ohm"),hh=Qh("magneticFluxQuantum","2.0678338484619295e-15","Wb"),dh=Qh("nuclearMagneton","5.0507837461e-27","J T^-1"),yh=Qh("klitzing","25812.807459304513","ohm"),gh=Qh("bohrRadius","5.29177210903e-11","m"),vh=Qh("classicalElectronRadius","2.8179403262e-15","m"),bh=Qh("electronMass","9.1093837015e-31","kg"),xh=Qh("fermiCoupling","1.1663787e-5","GeV^-2"),wh=Kh("fineStructure",.0072973525693),Nh=Qh("hartreeEnergy","4.3597447222071e-18","J"),Oh=Qh("protonMass","1.67262192369e-27","kg"),Mh=Qh("deuteronMass","3.3435830926e-27","kg"),Eh=Qh("neutronMass","1.6749271613e-27","kg"),jh=Qh("quantumOfCirculation","3.6369475516e-4","m^2 s^-1"),Sh=Qh("rydberg","10973731.568160","m^-1"),Ah=Qh("thomsonCrossSection","6.6524587321e-29","m^2"),Ch=Kh("weakMixingAngle",.2229),Th=Kh("efimovFactor",22.7),_h=Qh("atomicMass","1.66053906660e-27","kg"),Ih=Qh("avogadro","6.02214076e23","mol^-1"),qh=Qh("boltzmann","1.380649e-23","J K^-1"),Bh=Qh("faraday","96485.33212331001","C mol^-1"),kh=Qh("firstRadiation","3.7417718521927573e-16","W m^2"),zh=Qh("loschmidt","2.686780111798444e25","m^-3"),Dh=Qh("gasConstant","8.31446261815324","J K^-1 mol^-1"),Rh=Qh("molarPlanckConstant","3.990312712893431e-10","J s mol^-1"),Ph=Qh("molarVolume","0.022413969545014137","m^3 mol^-1"),Fh=Kh("sackurTetrode",-1.16487052358),Uh=Qh("secondRadiation","0.014387768775039337","m K"),Lh=Qh("stefanBoltzmann","5.67037441918443e-8","W m^-2 K^-4"),Hh=Qh("wienDisplacement","2.897771955e-3","m K"),$h=Qh("molarMass","0.99999999965e-3","kg mol^-1"),Gh=Qh("molarMassC12","11.9999999958e-3","kg mol^-1"),Zh=Qh("gravity","9.80665","m s^-2"),Vh=Qh("planckLength","1.616255e-35","m"),Jh=Qh("planckMass","2.176435e-8","kg"),Wh=Qh("planckTime","5.391245e-44","s"),Yh=Qh("planckCharge","1.87554603778e-18","C"),Xh=Qh("planckTemperature","1.416785e+32","K");function Qh(e,a,o){return Object(s.a)(e,["config","Unit","BigNumber"],function(e){var t=e.config,r=e.Unit,n=e.BigNumber,i=new r("BigNumber"===t.number?new n(a):parseFloat(a),o);return i.fixPrefix=!0,i})}function Kh(e,n){return Object(s.a)(e,["config","BigNumber"],function(e){var t=e.config,r=e.BigNumber;return"BigNumber"===t.number?new r(n):n})}var ed=["typed","isInteger"],td=Object(s.a)("apply",ed,function(e){var t=e.typed,r=e.isInteger,n=Et({typed:t,isInteger:r});return t("apply",{"...any":function(e){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1));try{return n.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),rd=["typed","Index","matrix","range"],nd=Object(s.a)("column",rd,function(e){var t=e.typed,r=e.Index,n=e.matrix,i=e.range,a=Hn({typed:t,Index:r,matrix:n,range:i});return t("column",{"...any":function(e){var t=e.length-1,r=e[t];Object(ie.y)(r)&&(e[t]=r-1);try{return a.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0});function id(e,t,r){var n=e.filter(function(e){return Object(ie.J)(e)&&!(e.name in t)&&!(e.name in r)})[0];if(!n)throw new Error('No undefined variable found in inline expression "'+e+'"');var i=n.name,a=Object.create(r),o=e.compile();return function(e){return a[i]=e,o.evaluate(a)}}var ad=["typed"],od=Object(s.a)("filter",ad,function(e){var t=e.typed;function r(e,t,r){var n,i;return e[0]&&(n=e[0].compile().evaluate(r)),e[1]&&(i=Object(ie.J)(e[1])||Object(ie.q)(e[1])?e[1].compile().evaluate(r):id(e[1],t,r)),a(n,i)}r.rawArgs=!0;var a=t("filter",{"Array, function":sd,"Matrix, function":function(e,t){return e.create(sd(e.toArray(),t))},"Array, RegExp":I.d,"Matrix, RegExp":function(e,t){return e.create(Object(I.d)(e.toArray(),t))}});return r},{isTransformFunction:!0});function sd(e,n){var i=Xn(n);return Object(I.c)(e,function(e,t,r){return 1===i?n(e):2===i?n(e,[t+1]):n(e,[t+1],r)})}var ud=["typed"],cd=Object(s.a)("forEach",ud,function(e){var t=e.typed;function r(e,t,r){var n,i;return e[0]&&(n=e[0].compile().evaluate(r)),e[1]&&(i=Object(ie.J)(e[1])||Object(ie.q)(e[1])?e[1].compile().evaluate(r):id(e[1],t,r)),a(n,i)}r.rawArgs=!0;var a=t("forEach",{"Array | Matrix, function":function(t,i){var a=Xn(i);!function r(e,n){Array.isArray(e)?Object(I.f)(e,function(e,t){r(e,n.concat(t+1))}):1===a?i(e):2===a?i(e,n):i(e,n,t)}(t.valueOf(),[])}});return r},{isTransformFunction:!0}),fd=["typed"],ld=Object(s.a)("map",fd,function(e){var t=e.typed;function r(e,t,r){var n,i;return e[0]&&(n=e[0].compile().evaluate(r)),e[1]&&(i=Object(ie.J)(e[1])||Object(ie.q)(e[1])?e[1].compile().evaluate(r):id(e[1],t,r)),a(n,i)}r.rawArgs=!0;var a=t("map",{"Array, function":function(e,t){return pd(e,t,e)},"Matrix, function":function(e,t){return e.create(pd(e.valueOf(),t,e))}});return r},{isTransformFunction:!0});function pd(e,t,i){var a=Xn(t);return function r(e,n){return Array.isArray(e)?Object(I.m)(e,function(e,t){return r(e,n.concat(t+1))}):1===a?t(e):2===a?t(e,n):t(e,n,i)}(e,[])}var md=["typed","larger"],hd=Object(s.a)("max",md,function(e){var t=e.typed,r=e.larger,n=os({typed:t,larger:r});return t("max",{"...any":function(e){if(2===e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return n.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),dd=["typed","add","divide"],yd=Object(s.a)("mean",dd,function(e){var t=e.typed,r=e.add,n=e.divide,i=rp({typed:t,add:r,divide:n});return t("mean",{"...any":function(e){if(2===e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return i.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),gd=["typed","smaller"],vd=Object(s.a)("min",gd,function(e){var t=e.typed,r=e.smaller,n=us({typed:t,smaller:r});return t("min",{"...any":function(e){if(2===e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return n.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),bd=["typed","config","?matrix","?bignumber","smaller","smallerEq","larger","largerEq"],xd=Object(s.a)("range",bd,function(e){var t=e.typed,r=e.config,n=e.matrix,i=e.bignumber,a=e.smaller,o=e.smallerEq,s=e.larger,u=e.largerEq,c=Ei({typed:t,config:r,matrix:n,bignumber:i,smaller:a,smallerEq:o,larger:s,largerEq:u});return t("range",{"...any":function(e){return"boolean"!=typeof e[e.length-1]&&e.push(!0),c.apply(null,e)}})},{isTransformFunction:!0}),wd=["typed","Index","matrix","range"],Nd=Object(s.a)("row",wd,function(e){var t=e.typed,r=e.Index,n=e.matrix,i=e.range,a=qi({typed:t,Index:r,matrix:n,range:i});return t("row",{"...any":function(e){var t=e.length-1,r=e[t];Object(ie.y)(r)&&(e[t]=r-1);try{return a.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),Od=["typed","matrix"],Md=Object(s.a)("subset",Od,function(e){var t=e.typed,r=e.matrix,n=Ji({typed:t,matrix:r});return t("subset",{"...any":function(e){try{return n.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),Ed=["typed","matrix","isInteger"],jd=Object(s.a)("concat",Ed,function(e){var t=e.typed,r=e.matrix,n=e.isInteger,i=Fn({typed:t,matrix:r,isInteger:n});return t("concat",{"...any":function(e){var t=e.length-1,r=e[t];Object(ie.y)(r)?e[t]=r-1:Object(ie.e)(r)&&(e[t]=r.minus(1));try{return i.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),Sd=["typed","sqrt","variance"],Ad=Object(s.a)("std",Sd,function(e){var t=e.typed,r=e.sqrt,n=e.variance,i=dp({typed:t,sqrt:r,variance:n});return t("std",{"...any":function(e){if(2<=e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return i.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),Cd=["typed","config","add","?bignumber","?fraction"],Td=Object(s.a)("sum",Cd,function(e){var t=e.typed,r=e.config,n=e.add,i=e.bignumber,a=e.fraction,o=ep({typed:t,config:r,add:n,bignumber:i,fraction:a});return t("sum",{"...any":function(e){if(2===e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return o.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0}),_d="variance",Id=["typed","add","subtract","multiply","divide","apply","isNaN"],qd=Object(s.a)(_d,Id,function(e){var t=e.typed,r=e.add,n=e.subtract,i=e.multiply,a=e.divide,o=e.apply,s=e.isNaN,u=fp({typed:t,add:r,subtract:n,multiply:i,divide:a,apply:o,isNaN:s});return t(_d,{"...any":function(e){if(2<=e.length&&Object(ie.i)(e[0])){var t=e[1];Object(ie.y)(t)?e[1]=t-1:Object(ie.e)(t)&&(e[1]=t.minus(1))}try{return u.apply(null,e)}catch(e){throw Rc(e)}}})},{isTransformFunction:!0});r.d(t,"createTyped",function(){return u}),r.d(t,"createResultSet",function(){return m}),r.d(t,"createBigNumberClass",function(){return g}),r.d(t,"createComplexClass",function(){return N}),r.d(t,"createFractionClass",function(){return S}),r.d(t,"createRangeClass",function(){return C}),r.d(t,"createMatrixClass",function(){return _}),r.d(t,"createDenseMatrixClass",function(){return B}),r.d(t,"createClone",function(){return z}),r.d(t,"createIsInteger",function(){return Z}),r.d(t,"createIsNegative",function(){return te}),r.d(t,"createIsNumeric",function(){return se}),r.d(t,"createHasNumericValue",function(){return fe}),r.d(t,"createIsPositive",function(){return me}),r.d(t,"createIsZero",function(){return de}),r.d(t,"createIsNaN",function(){return ge}),r.d(t,"createTypeOf",function(){return xe}),r.d(t,"createDeprecatedTypeof",function(){return we}),r.d(t,"createEqualScalar",function(){return Ee}),r.d(t,"createSparseMatrixClass",function(){return Se}),r.d(t,"createNumber",function(){return Ce}),r.d(t,"createString",function(){return _e}),r.d(t,"createBoolean",function(){return Be}),r.d(t,"createBignumber",function(){return ze}),r.d(t,"createComplex",function(){return Re}),r.d(t,"createFraction",function(){return Fe}),r.d(t,"createMatrix",function(){return Le}),r.d(t,"createSplitUnit",function(){return Ge}),r.d(t,"createUnaryMinus",function(){return vt}),r.d(t,"createUnaryPlus",function(){return wt}),r.d(t,"createAbs",function(){return Ot}),r.d(t,"createApply",function(){return Et}),r.d(t,"createAddScalar",function(){return Ct}),r.d(t,"createCbrt",function(){return _t}),r.d(t,"createCeil",function(){return qt}),r.d(t,"createCube",function(){return kt}),r.d(t,"createExp",function(){return Dt}),r.d(t,"createExpm1",function(){return Pt}),r.d(t,"createFix",function(){return Ut}),r.d(t,"createFloor",function(){return Ht}),r.d(t,"createGcd",function(){return tr}),r.d(t,"createLcm",function(){return cr}),r.d(t,"createLog10",function(){return lr}),r.d(t,"createLog2",function(){return mr}),r.d(t,"createMod",function(){return wr}),r.d(t,"createMultiplyScalar",function(){return Or}),r.d(t,"createMultiply",function(){return jr}),r.d(t,"createNthRoot",function(){return Cr}),r.d(t,"createSign",function(){return _r}),r.d(t,"createSqrt",function(){return qr}),r.d(t,"createSquare",function(){return kr}),r.d(t,"createSubtract",function(){return Rr}),r.d(t,"createXgcd",function(){return Ur}),r.d(t,"createDotMultiply",function(){return Zr}),r.d(t,"createBitAnd",function(){return ln}),r.d(t,"createBitNot",function(){return mn}),r.d(t,"createBitOr",function(){return dn}),r.d(t,"createBitXor",function(){return bn}),r.d(t,"createArg",function(){return wn}),r.d(t,"createConj",function(){return On}),r.d(t,"createIm",function(){return En}),r.d(t,"createRe",function(){return Sn}),r.d(t,"createNot",function(){return Bn}),r.d(t,"createOr",function(){return zn}),r.d(t,"createXor",function(){return Rn}),r.d(t,"createConcat",function(){return Fn}),r.d(t,"createColumn",function(){return Hn}),r.d(t,"createCross",function(){return Gn}),r.d(t,"createDiag",function(){return Vn}),r.d(t,"createEye",function(){return Jn}),r.d(t,"createFilter",function(){return Kn}),r.d(t,"createFlatten",function(){return ni}),r.d(t,"createForEach",function(){return oi}),r.d(t,"createGetMatrixDataType",function(){return fi}),r.d(t,"createIdentity",function(){return mi}),r.d(t,"createKron",function(){return di}),r.d(t,"createMap",function(){return gi}),r.d(t,"createOnes",function(){return xi}),r.d(t,"createRange",function(){return Ei}),r.d(t,"createReshape",function(){return Ai}),r.d(t,"createResize",function(){return _i}),r.d(t,"createRow",function(){return qi}),r.d(t,"createSize",function(){return ki}),r.d(t,"createSqueeze",function(){return Ri}),r.d(t,"createSubset",function(){return Ji}),r.d(t,"createTranspose",function(){return ea}),r.d(t,"createCtranspose",function(){return na}),r.d(t,"createZeros",function(){return aa}),r.d(t,"createErf",function(){return sa}),r.d(t,"createMode",function(){return ha}),r.d(t,"createProd",function(){return ga}),r.d(t,"createFormat",function(){return ba}),r.d(t,"createPrint",function(){return wa}),r.d(t,"createTo",function(){return Ma}),r.d(t,"createIsPrime",function(){return Sa}),r.d(t,"createNumeric",function(){return Ca}),r.d(t,"createDivideScalar",function(){return Ia}),r.d(t,"createPow",function(){return Ba}),r.d(t,"createRound",function(){return Fa}),r.d(t,"createLog",function(){return Ha}),r.d(t,"createLog1p",function(){return Ga}),r.d(t,"createNthRoots",function(){return Ja}),r.d(t,"createDotPow",function(){return Ya}),r.d(t,"createDotDivide",function(){return Ka}),r.d(t,"createLsolve",function(){return ro}),r.d(t,"createUsolve",function(){return io}),r.d(t,"createLeftShift",function(){return co}),r.d(t,"createRightArithShift",function(){return po}),r.d(t,"createRightLogShift",function(){return yo}),r.d(t,"createAnd",function(){return vo}),r.d(t,"createCompare",function(){return wo}),r.d(t,"createCompareNatural",function(){return jo}),r.d(t,"createCompareText",function(){return Co}),r.d(t,"createEqual",function(){return Io}),r.d(t,"createEqualText",function(){return ko}),r.d(t,"createSmaller",function(){return Ro}),r.d(t,"createSmallerEq",function(){return Uo}),r.d(t,"createLarger",function(){return $o}),r.d(t,"createLargerEq",function(){return Vo}),r.d(t,"createDeepEqual",function(){return Yo}),r.d(t,"createUnequal",function(){return Ko}),r.d(t,"createPartitionSelect",function(){return rs}),r.d(t,"createSort",function(){return is}),r.d(t,"createMax",function(){return os}),r.d(t,"createMin",function(){return us}),r.d(t,"createImmutableDenseMatrixClass",function(){return fs}),r.d(t,"createIndexClass",function(){return ps}),r.d(t,"createFibonacciHeapClass",function(){return hs}),r.d(t,"createSpaClass",function(){return ys}),r.d(t,"createUnitClass",function(){return Es}),r.d(t,"createUnitFunction",function(){return Ss}),r.d(t,"createSparse",function(){return Cs}),r.d(t,"createCreateUnit",function(){return Is}),r.d(t,"createAcos",function(){return Bs}),r.d(t,"createAcosh",function(){return Ks}),r.d(t,"createAcot",function(){return tu}),r.d(t,"createAcoth",function(){return nu}),r.d(t,"createAcsc",function(){return au}),r.d(t,"createAcsch",function(){return su}),r.d(t,"createAsec",function(){return cu}),r.d(t,"createAsech",function(){return lu}),r.d(t,"createAsin",function(){return mu}),r.d(t,"createAsinh",function(){return du}),r.d(t,"createAtan",function(){return gu}),r.d(t,"createAtan2",function(){return bu}),r.d(t,"createAtanh",function(){return wu}),r.d(t,"createCos",function(){return Ou}),r.d(t,"createCosh",function(){return Eu}),r.d(t,"createCot",function(){return Su}),r.d(t,"createCoth",function(){return Cu}),r.d(t,"createCsc",function(){return _u}),r.d(t,"createCsch",function(){return qu}),r.d(t,"createSec",function(){return ku}),r.d(t,"createSech",function(){return Du}),r.d(t,"createSin",function(){return Pu}),r.d(t,"createSinh",function(){return Uu}),r.d(t,"createTan",function(){return Hu}),r.d(t,"createTanh",function(){return Gu}),r.d(t,"createSetCartesian",function(){return Ju}),r.d(t,"createSetDifference",function(){return Xu}),r.d(t,"createSetDistinct",function(){return ec}),r.d(t,"createSetIntersect",function(){return nc}),r.d(t,"createSetIsSubset",function(){return oc}),r.d(t,"createSetMultiplicity",function(){return cc}),r.d(t,"createSetPowerset",function(){return pc}),r.d(t,"createSetSize",function(){return dc}),r.d(t,"createSetSymDifference",function(){return vc}),r.d(t,"createSetUnion",function(){return wc}),r.d(t,"createAdd",function(){return Oc}),r.d(t,"createHypot",function(){return Ec}),r.d(t,"createNorm",function(){return Sc}),r.d(t,"createDot",function(){return Cc}),r.d(t,"createTrace",function(){return _c}),r.d(t,"createIndex",function(){return qc}),r.d(t,"createNode",function(){return Dc}),r.d(t,"createAccessorNode",function(){return Lc}),r.d(t,"createArrayNode",function(){return $c}),r.d(t,"createAssignmentNode",function(){return Xc}),r.d(t,"createBlockNode",function(){return Kc}),r.d(t,"createConditionalNode",function(){return tf}),r.d(t,"createConstantNode",function(){return pf}),r.d(t,"createFunctionAssignmentNode",function(){return hf}),r.d(t,"createIndexNode",function(){return bf}),r.d(t,"createObjectNode",function(){return Nf}),r.d(t,"createOperatorNode",function(){return Mf}),r.d(t,"createParenthesisNode",function(){return jf}),r.d(t,"createRangeNode",function(){return Af}),r.d(t,"createRelationalNode",function(){return Tf}),r.d(t,"createSymbolNode",function(){return If}),r.d(t,"createFunctionNode",function(){return zf}),r.d(t,"createParse",function(){return Pf}),r.d(t,"createCompile",function(){return Lf}),r.d(t,"createEvaluate",function(){return Gf}),r.d(t,"createDeprecatedEval",function(){return Zf}),r.d(t,"createParserClass",function(){return Jf}),r.d(t,"createParser",function(){return Yf}),r.d(t,"createLup",function(){return Qf}),r.d(t,"createQr",function(){return el}),r.d(t,"createSlu",function(){return bl}),r.d(t,"createLusolve",function(){return Ol}),r.d(t,"createHelpClass",function(){return El}),r.d(t,"createChainClass",function(){return Sl}),r.d(t,"createHelp",function(){return kl}),r.d(t,"createChain",function(){return Dl}),r.d(t,"createDet",function(){return Pl}),r.d(t,"createInv",function(){return Ul}),r.d(t,"createExpm",function(){return Hl}),r.d(t,"createSqrtm",function(){return Gl}),r.d(t,"createDivide",function(){return Vl}),r.d(t,"createDistance",function(){return Yl}),r.d(t,"createIntersect",function(){return Ql}),r.d(t,"createSum",function(){return ep}),r.d(t,"createMean",function(){return rp}),r.d(t,"createMedian",function(){return ip}),r.d(t,"createMad",function(){return op}),r.d(t,"createVariance",function(){return fp}),r.d(t,"createDeprecatedVar",function(){return lp}),r.d(t,"createQuantileSeq",function(){return mp}),r.d(t,"createStd",function(){return dp}),r.d(t,"createCombinations",function(){return xp}),r.d(t,"createCombinationsWithRep",function(){return Mp}),r.d(t,"createGamma",function(){return Tp}),r.d(t,"createFactorial",function(){return qp}),r.d(t,"createKldivergence",function(){return zp}),r.d(t,"createMultinomial",function(){return Pp}),r.d(t,"createPermutations",function(){return Lp}),r.d(t,"createPickRandom",function(){return Wp}),r.d(t,"createRandom",function(){return Kp}),r.d(t,"createRandomInt",function(){return rm}),r.d(t,"createStirlingS2",function(){return am}),r.d(t,"createBellNumbers",function(){return um}),r.d(t,"createCatalan",function(){return lm}),r.d(t,"createComposition",function(){return hm}),r.d(t,"createSimplify",function(){return Em}),r.d(t,"createDerivative",function(){return Sm}),r.d(t,"createRationalize",function(){return Tm}),r.d(t,"createReviver",function(){return Im}),r.d(t,"createE",function(){return Hm}),r.d(t,"createUppercaseE",function(){return Km}),r.d(t,"createFalse",function(){return Dm}),r.d(t,"createI",function(){return Xm}),r.d(t,"createInfinity",function(){return Pm}),r.d(t,"createLN10",function(){return Zm}),r.d(t,"createLN2",function(){return Gm}),r.d(t,"createLOG10E",function(){return Jm}),r.d(t,"createLOG2E",function(){return Vm}),r.d(t,"createNaN",function(){return Fm}),r.d(t,"createNull",function(){return Rm}),r.d(t,"createPhi",function(){return $m}),r.d(t,"createPi",function(){return Um}),r.d(t,"createUppercasePi",function(){return Qm}),r.d(t,"createSQRT1_2",function(){return Wm}),r.d(t,"createSQRT2",function(){return Ym}),r.d(t,"createTau",function(){return Lm}),r.d(t,"createTrue",function(){return zm}),r.d(t,"createVersion",function(){return eh}),r.d(t,"createAtomicMass",function(){return _h}),r.d(t,"createAvogadro",function(){return Ih}),r.d(t,"createBohrMagneton",function(){return lh}),r.d(t,"createBohrRadius",function(){return gh}),r.d(t,"createBoltzmann",function(){return qh}),r.d(t,"createClassicalElectronRadius",function(){return vh}),r.d(t,"createConductanceQuantum",function(){return ph}),r.d(t,"createCoulomb",function(){return ch}),r.d(t,"createDeuteronMass",function(){return Mh}),r.d(t,"createEfimovFactor",function(){return Th}),r.d(t,"createElectricConstant",function(){return sh}),r.d(t,"createElectronMass",function(){return bh}),r.d(t,"createElementaryCharge",function(){return fh}),r.d(t,"createFaraday",function(){return Bh}),r.d(t,"createFermiCoupling",function(){return xh}),r.d(t,"createFineStructure",function(){return wh}),r.d(t,"createFirstRadiation",function(){return kh}),r.d(t,"createGasConstant",function(){return Dh}),r.d(t,"createGravitationConstant",function(){return nh}),r.d(t,"createGravity",function(){return Zh}),r.d(t,"createHartreeEnergy",function(){return Nh}),r.d(t,"createInverseConductanceQuantum",function(){return mh}),r.d(t,"createKlitzing",function(){return yh}),r.d(t,"createLoschmidt",function(){return zh}),r.d(t,"createMagneticConstant",function(){return oh}),r.d(t,"createMagneticFluxQuantum",function(){return hh}),r.d(t,"createMolarMass",function(){return $h}),r.d(t,"createMolarMassC12",function(){return Gh}),r.d(t,"createMolarPlanckConstant",function(){return Rh}),r.d(t,"createMolarVolume",function(){return Ph}),r.d(t,"createNeutronMass",function(){return Eh}),r.d(t,"createNuclearMagneton",function(){return dh}),r.d(t,"createPlanckCharge",function(){return Yh}),r.d(t,"createPlanckConstant",function(){return ih}),r.d(t,"createPlanckLength",function(){return Vh}),r.d(t,"createPlanckMass",function(){return Jh}),r.d(t,"createPlanckTemperature",function(){return Xh}),r.d(t,"createPlanckTime",function(){return Wh}),r.d(t,"createProtonMass",function(){return Oh}),r.d(t,"createQuantumOfCirculation",function(){return jh}),r.d(t,"createReducedPlanckConstant",function(){return ah}),r.d(t,"createRydberg",function(){return Sh}),r.d(t,"createSackurTetrode",function(){return Fh}),r.d(t,"createSecondRadiation",function(){return Uh}),r.d(t,"createSpeedOfLight",function(){return rh}),r.d(t,"createStefanBoltzmann",function(){return Lh}),r.d(t,"createThomsonCrossSection",function(){return Ah}),r.d(t,"createVacuumImpedance",function(){return uh}),r.d(t,"createWeakMixingAngle",function(){return Ch}),r.d(t,"createWienDisplacement",function(){return Hh}),r.d(t,"createApplyTransform",function(){return td}),r.d(t,"createColumnTransform",function(){return nd}),r.d(t,"createFilterTransform",function(){return od}),r.d(t,"createForEachTransform",function(){return cd}),r.d(t,"createIndexTransform",function(){return yf}),r.d(t,"createMapTransform",function(){return ld}),r.d(t,"createMaxTransform",function(){return hd}),r.d(t,"createMeanTransform",function(){return yd}),r.d(t,"createMinTransform",function(){return vd}),r.d(t,"createRangeTransform",function(){return xd}),r.d(t,"createRowTransform",function(){return Nd}),r.d(t,"createSubsetTransform",function(){return Md}),r.d(t,"createConcatTransform",function(){return jd}),r.d(t,"createStdTransform",function(){return Ad}),r.d(t,"createSumTransform",function(){return Td}),r.d(t,"createVarianceTransform",function(){return qd})},function(e,t,r){"use strict";r.r(t);var v=r(3),n=r(18),i=r.n(n);var b=r(1),x=r(0),w=r(2),N=r(13),O=r(8);function M(e){return(M="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function c(u,c,f,l){function s(e,t,r){if(r.wrap&&"function"==typeof t&&(t=function(i){function e(){for(var e=[],t=0,r=arguments.length;t<r;t++){var n=arguments[t];e[t]=n&&n.valueOf()}return i.apply(f,e)}i.transform&&(e.transform=i.transform);return e}(t)),function(e){return"function"==typeof e&&"string"==typeof e.signature}(t)&&(t=u(e,function(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}({},t.signature,t))),h(f[e])&&h(t))return t=r.override?u(e,t.signatures):u(f[e],t),f[e]=t,delete l[e],n(e,t),void f.emit("import",e,function(){return t});if(void 0===f[e]||r.override)return f[e]=t,delete l[e],n(e,t),void f.emit("import",e,function(){return t});if(!r.silent)throw new Error('Cannot import "'+e+'": already exists')}function n(e,t){t&&"function"==typeof t.transform?(f.expression.transform[e]=t.transform,r(e)&&(f.expression.mathWithTransform[e]=t.transform)):(delete f.expression.transform[e],r(e)&&(f.expression.mathWithTransform[e]=t))}function p(e){delete f.expression.transform[e],r(e)?f.expression.mathWithTransform[e]=f[e]:delete f.expression.mathWithTransform[e]}function m(r,n,e){var i=2<arguments.length&&void 0!==e?e:r.fn;if(Object(w.b)(i,"."))throw new Error("Factory name should not contain a nested path. Name: "+JSON.stringify(i));function t(){var t={};r.dependencies.map(x.c).forEach(function(e){if(Object(w.b)(e,"."))throw new Error("Factory dependency should not contain a nested path. Name: "+JSON.stringify(e));"math"===e?t.math=f:"mathWithTransform"===e?t.mathWithTransform=f.expression.mathWithTransform:"classes"===e?t.classes=f:t[e]=f[e]});var e=r(t);if(e&&"function"==typeof e.transform)throw new Error('Transforms cannot be attached to factory functions. Please create a separate function for it with exports.path="expression.transform"');if(void 0===s||n.override)return e;if(h(s)&&h(e))return u(s,e);if(n.silent)return s;throw new Error('Cannot import "'+i+'": already exists')}var a=g(r)?f.expression.transform:f,o=i in f.expression.transform,s=Object(v.f)(a,i)?a[i]:void 0;r.meta&&!1===r.meta.lazy?a[i]=t():Object(v.h)(a,i,t),s&&o?p(i):(g(r)||y(r))&&Object(v.h)(f.expression.mathWithTransform,i,function(){return a[i]}),l[i]=r,f.emit("import",i,t)}function h(e){return"function"==typeof e&&"object"===M(e.signatures)}function r(e){return!Object(v.f)(t,e)}function d(e){return void 0===e.path&&!Object(v.f)(t,e.name)}function y(e){return!(-1!==e.fn.indexOf(".")||Object(v.f)(t,e.fn)||e.meta&&e.meta.isClass)}function g(e){return void 0!==e&&void 0!==e.meta&&!0===e.meta.isTransformFunction||!1}var t={expression:!0,type:!0,docs:!0,error:!0,json:!0,chain:!0};return function(e,o){var t=arguments.length;if(1!==t&&2!==t)throw new N.a("import",t,1,2);o=o||{};var r,n={};for(var i in function t(r,e,n){if(Object(v.g)(e))!function(t,r){if(Object(O.a)("Factories of type { name, factory } are deprecated since v6. Please create your factory functions using the math.factory function."),"string"==typeof t.name){var n=t.name,e=n in f.expression.transform,i=t.path?Object(v.k)(f,t.path):f,a=Object(v.f)(i,n)?i[n]:void 0,o=function(){var e=c(t);if(e&&"function"==typeof e.transform)throw new Error('Transforms cannot be attached to factory functions. Please create a separate function for it with exports.path="expression.transform"');if(h(a)&&h(e))return r.override||(e=u(a,e)),e;if(void 0===a||r.override)return e;if(r.silent)return a;throw new Error('Cannot import "'+n+'": already exists')};!1!==t.lazy?(Object(v.h)(i,n,o),e?p(n):"expression.transform"!==t.path&&!d(t)||Object(v.h)(f.expression.mathWithTransform,n,o)):(i[n]=o(),e?p(n):"expression.transform"!==t.path&&!d(t)||(f.expression.mathWithTransform[n]=o()));var s=t.path?t.path+"."+t.name:t.name;l[s]=t,f.emit("import",n,o,t.path)}else c(t)}(e,o);else if(Array.isArray(e))e.forEach(function(e){return t(r,e)});else if("object"===M(e))for(var i in e)Object(v.f)(e,i)&&t(r,e[i],i);else if(Object(x.b)(e)||void 0!==n){var a=Object(x.b)(e)?g(e)?e.fn+".transform":e.fn:n;if(Object(v.f)(r,a)&&r[a]!==e&&!o.silent)throw new Error('Cannot import "'+a+'" twice');r[a]=e}else if(!o.silent)throw new TypeError("Factory, Object, or Array expected")}(n,e),n)if(Object(v.f)(n,i)){var a=n[i];if(Object(x.b)(a))m(a,o);else if("function"==typeof(r=a)||"number"==typeof r||"string"==typeof r||"boolean"==typeof r||null===r||Object(b.L)(r)||Object(b.j)(r)||Object(b.e)(r)||Object(b.o)(r)||Object(b.v)(r)||Array.isArray(r))s(i,a,o);else if(!o.silent)throw new TypeError("Factory, Object, or Array expected")}}}var f={epsilon:1e-12,matrix:"Matrix",number:"number",precision:64,predictable:!1,randomSeed:null},l=["Matrix","Array"],p=["number","BigNumber","Fraction"];function m(e,t,r){if(void 0!==e[t]&&!function(e,t){return-1!==e.indexOf(t)}(r,e[t])){var n=function(e,t){return e.map(function(e){return e.toLowerCase()}).indexOf(t.toLowerCase())}(r,e[t]);-1!==n?(console.warn('Warning: Wrong casing for configuration option "'+t+'", should be "'+r[n]+'" instead of "'+e[t]+'".'),e[t]=r[n]):console.warn('Warning: Unknown value "'+e[t]+'" for configuration option "'+t+'". Available options: '+r.map(JSON.stringify).join(", ")+".")}}var h=r(6),d=r(10);function y(){return(y=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}function g(e,t){var a=y({},f,t);if("function"!=typeof Object.create)throw new Error("ES5 not supported by this JavaScript engine. Please load the es5-shim and es5-sham library for compatibility.");var o=function(e){var t=new i.a;return e.on=t.on.bind(t),e.off=t.off.bind(t),e.once=t.once.bind(t),e.emit=t.emit.bind(t),e}({isNumber:b.y,isComplex:b.j,isBigNumber:b.e,isFraction:b.o,isUnit:b.L,isString:b.I,isArray:b.b,isMatrix:b.v,isCollection:b.i,isDenseMatrix:b.n,isSparseMatrix:b.H,isRange:b.D,isIndex:b.t,isBoolean:b.g,isResultSet:b.G,isHelp:b.s,isFunction:b.p,isDate:b.m,isRegExp:b.F,isObject:b.z,isNull:b.x,isUndefined:b.K,isAccessorNode:b.a,isArrayNode:b.c,isAssignmentNode:b.d,isBlockNode:b.f,isConditionalNode:b.k,isConstantNode:b.l,isFunctionAssignmentNode:b.q,isFunctionNode:b.r,isIndexNode:b.u,isNode:b.w,isObjectNode:b.A,isOperatorNode:b.B,isParenthesisNode:b.C,isRangeNode:b.E,isSymbolNode:b.J,isChain:b.h});o.config=function(i,a){function t(e){if(e){var t=Object(v.i)(i,v.a);m(e,"matrix",l),m(e,"number",p),Object(v.b)(i,e);var r=Object(v.i)(i,v.a),n=Object(v.i)(e,v.a);return a("config",r,t,n),r}return Object(v.i)(i,v.a)}return t.MATRIX_OPTIONS=l,t.NUMBER_OPTIONS=p,Object.keys(f).forEach(function(e){Object.defineProperty(t,e,{get:function(){return i[e]},enumerable:!0,configurable:!0})}),t}(a,o.emit),o.expression={transform:{},mathWithTransform:{config:o.config}};var s=[],u=[];var r={};var n=c(function(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return o.typed.apply(o.typed,t)},function e(t){if(Object(x.b)(t))return t(o);var r=t[Object.keys(t)[0]];if(Object(x.b)(r))return r(o);if(!Object(v.g)(t))throw console.warn("Factory object with properties `type`, `name`, and `factory` expected",t),new Error("Factory object with properties `type`, `name`, and `factory` expected");var n,i=s.indexOf(t);return-1===i?(n=!0===t.math?t.factory(o.type,a,e,o.typed,o):t.factory(o.type,a,e,o.typed),s.push(t),u.push(n)):n=u[i],n},o,r);o.import=n,o.on("config",function(){Object(v.l)(r).forEach(function(e){e&&e.meta&&e.meta.recreateOnConfigChange&&n(e,{override:!0})})}),o.create=g.bind(null,e),o.factory=x.a,o.import(Object(v.l)(Object(v.c)(e)));return["type.isNumber","type.isComplex","type.isBigNumber","type.isFraction","type.isUnit","type.isString","type.isArray","type.isMatrix","type.isDenseMatrix","type.isSparseMatrix","type.isCollection","type.isRange","type.isIndex","type.isBoolean","type.isResultSet","type.isHelp","type.isFunction","type.isDate","type.isRegExp","type.isObject","type.isNull","type.isUndefined","type.isAccessorNode","type.isArrayNode","type.isAssignmentNode","type.isBlockNode","type.isConditionalNode","type.isConstantNode","type.isFunctionAssignmentNode","type.isFunctionNode","type.isIndexNode","type.isNode","type.isObjectNode","type.isOperatorNode","type.isParenthesisNode","type.isRangeNode","type.isSymbolNode","type.isChain","type.BigNumber","type.Chain","type.Complex","type.Fraction","type.Matrix","type.DenseMatrix","type.SparseMatrix","type.Spa","type.FibonacciHeap","type.ImmutableDenseMatrix","type.Index","type.Range","type.ResultSet","type.Unit","type.Help","type.Parser","expression.parse","expression.Parser","expression.node.AccessorNode","expression.node.ArrayNode","expression.node.AssignmentNode","expression.node.BlockNode","expression.node.ConditionalNode","expression.node.ConstantNode","expression.node.IndexNode","expression.node.FunctionAssignmentNode","expression.node.FunctionNode","expression.node.Node","expression.node.ObjectNode","expression.node.OperatorNode","expression.node.ParenthesisNode","expression.node.RangeNode","expression.node.RelationalNode","expression.node.SymbolNode","json.reviver","error.ArgumentsError","error.DimensionError","error.IndexError"].forEach(function(e){var t=e.split("."),r=Object(w.j)(t),n=Object(w.l)(t),i=Object(v.k)(o,r);Object(v.h)(i,n,function(){return Object(O.a)("math.".concat(e," is moved to math.").concat(n," in v6.0.0. ")+"Please use the new location instead."),o[n]})}),Object(v.h)(o.expression,"docs",function(){throw new Error("math.expression.docs has been moved. Please import via \"import { docs } from 'mathjs'\"")}),o.ArgumentsError=N.a,o.DimensionError=h.a,o.IndexError=d.a,o}r.d(t,"create",function(){return g})}],i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)i.d(r,n,function(e){return t[e]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=19);function i(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,i),t.l=!0,t.exports}var r,n});
+//# sourceMappingURL=math.min.map
diff --git a/system/javascript/osapjs/client/libs/poly.js b/system/javascript/osapjs/client/libs/poly.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe5dcd086fa8e81f2f034847d3c61324e3492f66
--- /dev/null
+++ b/system/javascript/osapjs/client/libs/poly.js
@@ -0,0 +1,5704 @@
+(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+  (function (global){(function (){
+  const RegressionMultivariatePolynomial = require('regression-multivariate-polynomial');
+  
+  global.window.RegressionMultivariatePolynomial = RegressionMultivariatePolynomial
+  }).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+  },{"regression-multivariate-polynomial":7}],2:[function(require,module,exports){
+  "use strict";
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.isAnyArray = void 0;
+  const toString = Object.prototype.toString;
+  /**
+   * Checks if an object is an instance of an Array (array or typed array).
+   *
+   * @param {any} value - Object to check.
+   * @returns {boolean} True if the object is an array.
+   */
+  function isAnyArray(value) {
+      return toString.call(value).endsWith('Array]');
+  }
+  exports.isAnyArray = isAnyArray;
+  
+  },{}],3:[function(require,module,exports){
+  'use strict';
+  
+  var isAnyArray = require('is-any-array');
+  
+  function max(input, options = {}) {
+    if (!isAnyArray.isAnyArray(input)) {
+      throw new TypeError('input must be an array');
+    }
+  
+    if (input.length === 0) {
+      throw new TypeError('input must not be empty');
+    }
+  
+    const { fromIndex = 0, toIndex = input.length } = options;
+  
+    if (
+      fromIndex < 0 ||
+      fromIndex >= input.length ||
+      !Number.isInteger(fromIndex)
+    ) {
+      throw new Error('fromIndex must be a positive integer smaller than length');
+    }
+  
+    if (
+      toIndex <= fromIndex ||
+      toIndex > input.length ||
+      !Number.isInteger(toIndex)
+    ) {
+      throw new Error(
+        'toIndex must be an integer greater than fromIndex and at most equal to length',
+      );
+    }
+  
+    let maxValue = input[fromIndex];
+    for (let i = fromIndex + 1; i < toIndex; i++) {
+      if (input[i] > maxValue) maxValue = input[i];
+    }
+    return maxValue;
+  }
+  
+  module.exports = max;
+  
+  },{"is-any-array":2}],4:[function(require,module,exports){
+  'use strict';
+  
+  var isAnyArray = require('is-any-array');
+  
+  function min(input, options = {}) {
+    if (!isAnyArray.isAnyArray(input)) {
+      throw new TypeError('input must be an array');
+    }
+  
+    if (input.length === 0) {
+      throw new TypeError('input must not be empty');
+    }
+  
+    const { fromIndex = 0, toIndex = input.length } = options;
+  
+    if (
+      fromIndex < 0 ||
+      fromIndex >= input.length ||
+      !Number.isInteger(fromIndex)
+    ) {
+      throw new Error('fromIndex must be a positive integer smaller than length');
+    }
+  
+    if (
+      toIndex <= fromIndex ||
+      toIndex > input.length ||
+      !Number.isInteger(toIndex)
+    ) {
+      throw new Error(
+        'toIndex must be an integer greater than fromIndex and at most equal to length',
+      );
+    }
+  
+    let minValue = input[fromIndex];
+    for (let i = fromIndex + 1; i < toIndex; i++) {
+      if (input[i] < minValue) minValue = input[i];
+    }
+    return minValue;
+  }
+  
+  module.exports = min;
+  
+  },{"is-any-array":2}],5:[function(require,module,exports){
+  'use strict';
+  
+  var isAnyArray = require('is-any-array');
+  var max = require('ml-array-max');
+  var min = require('ml-array-min');
+  
+  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
+  
+  var max__default = /*#__PURE__*/_interopDefaultLegacy(max);
+  var min__default = /*#__PURE__*/_interopDefaultLegacy(min);
+  
+  function rescale(input, options = {}) {
+    if (!isAnyArray.isAnyArray(input)) {
+      throw new TypeError('input must be an array');
+    } else if (input.length === 0) {
+      throw new TypeError('input must not be empty');
+    }
+  
+    let output;
+    if (options.output !== undefined) {
+      if (!isAnyArray.isAnyArray(options.output)) {
+        throw new TypeError('output option must be an array if specified');
+      }
+      output = options.output;
+    } else {
+      output = new Array(input.length);
+    }
+  
+    const currentMin = min__default['default'](input);
+    const currentMax = max__default['default'](input);
+  
+    if (currentMin === currentMax) {
+      throw new RangeError(
+        'minimum and maximum input values are equal. Cannot rescale a constant array',
+      );
+    }
+  
+    const {
+      min: minValue = options.autoMinMax ? currentMin : 0,
+      max: maxValue = options.autoMinMax ? currentMax : 1,
+    } = options;
+  
+    if (minValue >= maxValue) {
+      throw new RangeError('min option must be smaller than max option');
+    }
+  
+    const factor = (maxValue - minValue) / (currentMax - currentMin);
+    for (let i = 0; i < input.length; i++) {
+      output[i] = (input[i] - currentMin) * factor + minValue;
+    }
+  
+    return output;
+  }
+  
+  module.exports = rescale;
+  
+  },{"is-any-array":2,"ml-array-max":3,"ml-array-min":4}],6:[function(require,module,exports){
+  'use strict';
+  
+  Object.defineProperty(exports, '__esModule', { value: true });
+  
+  var isAnyArray = require('is-any-array');
+  var rescale = require('ml-array-rescale');
+  
+  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
+  
+  var rescale__default = /*#__PURE__*/_interopDefaultLegacy(rescale);
+  
+  const indent = ' '.repeat(2);
+  const indentData = ' '.repeat(4);
+  
+  function inspectMatrix() {
+    return inspectMatrixWithOptions(this);
+  }
+  
+  function inspectMatrixWithOptions(matrix, options = {}) {
+    const { maxRows = 15, maxColumns = 10, maxNumSize = 8 } = options;
+    return `${matrix.constructor.name} {
+  ${indent}[
+  ${indentData}${inspectData(matrix, maxRows, maxColumns, maxNumSize)}
+  ${indent}]
+  ${indent}rows: ${matrix.rows}
+  ${indent}columns: ${matrix.columns}
+  }`;
+  }
+  
+  function inspectData(matrix, maxRows, maxColumns, maxNumSize) {
+    const { rows, columns } = matrix;
+    const maxI = Math.min(rows, maxRows);
+    const maxJ = Math.min(columns, maxColumns);
+    const result = [];
+    for (let i = 0; i < maxI; i++) {
+      let line = [];
+      for (let j = 0; j < maxJ; j++) {
+        line.push(formatNumber(matrix.get(i, j), maxNumSize));
+      }
+      result.push(`${line.join(' ')}`);
+    }
+    if (maxJ !== columns) {
+      result[result.length - 1] += ` ... ${columns - maxColumns} more columns`;
+    }
+    if (maxI !== rows) {
+      result.push(`... ${rows - maxRows} more rows`);
+    }
+    return result.join(`\n${indentData}`);
+  }
+  
+  function formatNumber(num, maxNumSize) {
+    const numStr = String(num);
+    if (numStr.length <= maxNumSize) {
+      return numStr.padEnd(maxNumSize, ' ');
+    }
+    const precise = num.toPrecision(maxNumSize - 2);
+    if (precise.length <= maxNumSize) {
+      return precise;
+    }
+    const exponential = num.toExponential(maxNumSize - 2);
+    const eIndex = exponential.indexOf('e');
+    const e = exponential.slice(eIndex);
+    return exponential.slice(0, maxNumSize - e.length) + e;
+  }
+  
+  function installMathOperations(AbstractMatrix, Matrix) {
+    AbstractMatrix.prototype.add = function add(value) {
+      if (typeof value === 'number') return this.addS(value);
+      return this.addM(value);
+    };
+  
+    AbstractMatrix.prototype.addS = function addS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) + value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.addM = function addM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) + matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.add = function add(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.add(value);
+    };
+  
+    AbstractMatrix.prototype.sub = function sub(value) {
+      if (typeof value === 'number') return this.subS(value);
+      return this.subM(value);
+    };
+  
+    AbstractMatrix.prototype.subS = function subS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) - value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.subM = function subM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) - matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.sub = function sub(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.sub(value);
+    };
+    AbstractMatrix.prototype.subtract = AbstractMatrix.prototype.sub;
+    AbstractMatrix.prototype.subtractS = AbstractMatrix.prototype.subS;
+    AbstractMatrix.prototype.subtractM = AbstractMatrix.prototype.subM;
+    AbstractMatrix.subtract = AbstractMatrix.sub;
+  
+    AbstractMatrix.prototype.mul = function mul(value) {
+      if (typeof value === 'number') return this.mulS(value);
+      return this.mulM(value);
+    };
+  
+    AbstractMatrix.prototype.mulS = function mulS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) * value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.mulM = function mulM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) * matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.mul = function mul(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.mul(value);
+    };
+    AbstractMatrix.prototype.multiply = AbstractMatrix.prototype.mul;
+    AbstractMatrix.prototype.multiplyS = AbstractMatrix.prototype.mulS;
+    AbstractMatrix.prototype.multiplyM = AbstractMatrix.prototype.mulM;
+    AbstractMatrix.multiply = AbstractMatrix.mul;
+  
+    AbstractMatrix.prototype.div = function div(value) {
+      if (typeof value === 'number') return this.divS(value);
+      return this.divM(value);
+    };
+  
+    AbstractMatrix.prototype.divS = function divS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) / value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.divM = function divM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) / matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.div = function div(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.div(value);
+    };
+    AbstractMatrix.prototype.divide = AbstractMatrix.prototype.div;
+    AbstractMatrix.prototype.divideS = AbstractMatrix.prototype.divS;
+    AbstractMatrix.prototype.divideM = AbstractMatrix.prototype.divM;
+    AbstractMatrix.divide = AbstractMatrix.div;
+  
+    AbstractMatrix.prototype.mod = function mod(value) {
+      if (typeof value === 'number') return this.modS(value);
+      return this.modM(value);
+    };
+  
+    AbstractMatrix.prototype.modS = function modS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) % value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.modM = function modM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) % matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.mod = function mod(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.mod(value);
+    };
+    AbstractMatrix.prototype.modulus = AbstractMatrix.prototype.mod;
+    AbstractMatrix.prototype.modulusS = AbstractMatrix.prototype.modS;
+    AbstractMatrix.prototype.modulusM = AbstractMatrix.prototype.modM;
+    AbstractMatrix.modulus = AbstractMatrix.mod;
+  
+    AbstractMatrix.prototype.and = function and(value) {
+      if (typeof value === 'number') return this.andS(value);
+      return this.andM(value);
+    };
+  
+    AbstractMatrix.prototype.andS = function andS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) & value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.andM = function andM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) & matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.and = function and(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.and(value);
+    };
+  
+    AbstractMatrix.prototype.or = function or(value) {
+      if (typeof value === 'number') return this.orS(value);
+      return this.orM(value);
+    };
+  
+    AbstractMatrix.prototype.orS = function orS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) | value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.orM = function orM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) | matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.or = function or(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.or(value);
+    };
+  
+    AbstractMatrix.prototype.xor = function xor(value) {
+      if (typeof value === 'number') return this.xorS(value);
+      return this.xorM(value);
+    };
+  
+    AbstractMatrix.prototype.xorS = function xorS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) ^ value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.xorM = function xorM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) ^ matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.xor = function xor(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.xor(value);
+    };
+  
+    AbstractMatrix.prototype.leftShift = function leftShift(value) {
+      if (typeof value === 'number') return this.leftShiftS(value);
+      return this.leftShiftM(value);
+    };
+  
+    AbstractMatrix.prototype.leftShiftS = function leftShiftS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) << value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.leftShiftM = function leftShiftM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) << matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.leftShift = function leftShift(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.leftShift(value);
+    };
+  
+    AbstractMatrix.prototype.signPropagatingRightShift = function signPropagatingRightShift(value) {
+      if (typeof value === 'number') return this.signPropagatingRightShiftS(value);
+      return this.signPropagatingRightShiftM(value);
+    };
+  
+    AbstractMatrix.prototype.signPropagatingRightShiftS = function signPropagatingRightShiftS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) >> value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.signPropagatingRightShiftM = function signPropagatingRightShiftM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) >> matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.signPropagatingRightShift = function signPropagatingRightShift(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.signPropagatingRightShift(value);
+    };
+  
+    AbstractMatrix.prototype.rightShift = function rightShift(value) {
+      if (typeof value === 'number') return this.rightShiftS(value);
+      return this.rightShiftM(value);
+    };
+  
+    AbstractMatrix.prototype.rightShiftS = function rightShiftS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) >>> value);
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.rightShiftM = function rightShiftM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) >>> matrix.get(i, j));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.rightShift = function rightShift(matrix, value) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.rightShift(value);
+    };
+    AbstractMatrix.prototype.zeroFillRightShift = AbstractMatrix.prototype.rightShift;
+    AbstractMatrix.prototype.zeroFillRightShiftS = AbstractMatrix.prototype.rightShiftS;
+    AbstractMatrix.prototype.zeroFillRightShiftM = AbstractMatrix.prototype.rightShiftM;
+    AbstractMatrix.zeroFillRightShift = AbstractMatrix.rightShift;
+  
+    AbstractMatrix.prototype.not = function not() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, ~(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.not = function not(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.not();
+    };
+  
+    AbstractMatrix.prototype.abs = function abs() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.abs(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.abs = function abs(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.abs();
+    };
+  
+    AbstractMatrix.prototype.acos = function acos() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.acos(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.acos = function acos(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.acos();
+    };
+  
+    AbstractMatrix.prototype.acosh = function acosh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.acosh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.acosh = function acosh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.acosh();
+    };
+  
+    AbstractMatrix.prototype.asin = function asin() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.asin(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.asin = function asin(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.asin();
+    };
+  
+    AbstractMatrix.prototype.asinh = function asinh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.asinh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.asinh = function asinh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.asinh();
+    };
+  
+    AbstractMatrix.prototype.atan = function atan() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.atan(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.atan = function atan(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.atan();
+    };
+  
+    AbstractMatrix.prototype.atanh = function atanh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.atanh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.atanh = function atanh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.atanh();
+    };
+  
+    AbstractMatrix.prototype.cbrt = function cbrt() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.cbrt(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.cbrt = function cbrt(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.cbrt();
+    };
+  
+    AbstractMatrix.prototype.ceil = function ceil() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.ceil(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.ceil = function ceil(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.ceil();
+    };
+  
+    AbstractMatrix.prototype.clz32 = function clz32() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.clz32(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.clz32 = function clz32(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.clz32();
+    };
+  
+    AbstractMatrix.prototype.cos = function cos() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.cos(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.cos = function cos(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.cos();
+    };
+  
+    AbstractMatrix.prototype.cosh = function cosh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.cosh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.cosh = function cosh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.cosh();
+    };
+  
+    AbstractMatrix.prototype.exp = function exp() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.exp(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.exp = function exp(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.exp();
+    };
+  
+    AbstractMatrix.prototype.expm1 = function expm1() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.expm1(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.expm1 = function expm1(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.expm1();
+    };
+  
+    AbstractMatrix.prototype.floor = function floor() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.floor(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.floor = function floor(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.floor();
+    };
+  
+    AbstractMatrix.prototype.fround = function fround() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.fround(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.fround = function fround(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.fround();
+    };
+  
+    AbstractMatrix.prototype.log = function log() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.log(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.log = function log(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.log();
+    };
+  
+    AbstractMatrix.prototype.log1p = function log1p() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.log1p(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.log1p = function log1p(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.log1p();
+    };
+  
+    AbstractMatrix.prototype.log10 = function log10() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.log10(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.log10 = function log10(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.log10();
+    };
+  
+    AbstractMatrix.prototype.log2 = function log2() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.log2(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.log2 = function log2(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.log2();
+    };
+  
+    AbstractMatrix.prototype.round = function round() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.round(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.round = function round(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.round();
+    };
+  
+    AbstractMatrix.prototype.sign = function sign() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.sign(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.sign = function sign(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.sign();
+    };
+  
+    AbstractMatrix.prototype.sin = function sin() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.sin(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.sin = function sin(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.sin();
+    };
+  
+    AbstractMatrix.prototype.sinh = function sinh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.sinh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.sinh = function sinh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.sinh();
+    };
+  
+    AbstractMatrix.prototype.sqrt = function sqrt() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.sqrt(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.sqrt = function sqrt(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.sqrt();
+    };
+  
+    AbstractMatrix.prototype.tan = function tan() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.tan(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.tan = function tan(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.tan();
+    };
+  
+    AbstractMatrix.prototype.tanh = function tanh() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.tanh(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.tanh = function tanh(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.tanh();
+    };
+  
+    AbstractMatrix.prototype.trunc = function trunc() {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.trunc(this.get(i, j)));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.trunc = function trunc(matrix) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.trunc();
+    };
+  
+    AbstractMatrix.pow = function pow(matrix, arg0) {
+      const newMatrix = new Matrix(matrix);
+      return newMatrix.pow(arg0);
+    };
+  
+    AbstractMatrix.prototype.pow = function pow(value) {
+      if (typeof value === 'number') return this.powS(value);
+      return this.powM(value);
+    };
+  
+    AbstractMatrix.prototype.powS = function powS(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.pow(this.get(i, j), value));
+        }
+      }
+      return this;
+    };
+  
+    AbstractMatrix.prototype.powM = function powM(matrix) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (this.rows !== matrix.rows ||
+        this.columns !== matrix.columns) {
+        throw new RangeError('Matrices dimensions must be equal');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, Math.pow(this.get(i, j), matrix.get(i, j)));
+        }
+      }
+      return this;
+    };
+  }
+  
+  /**
+   * @private
+   * Check that a row index is not out of bounds
+   * @param {Matrix} matrix
+   * @param {number} index
+   * @param {boolean} [outer]
+   */
+  function checkRowIndex(matrix, index, outer) {
+    let max = outer ? matrix.rows : matrix.rows - 1;
+    if (index < 0 || index > max) {
+      throw new RangeError('Row index out of range');
+    }
+  }
+  
+  /**
+   * @private
+   * Check that a column index is not out of bounds
+   * @param {Matrix} matrix
+   * @param {number} index
+   * @param {boolean} [outer]
+   */
+  function checkColumnIndex(matrix, index, outer) {
+    let max = outer ? matrix.columns : matrix.columns - 1;
+    if (index < 0 || index > max) {
+      throw new RangeError('Column index out of range');
+    }
+  }
+  
+  /**
+   * @private
+   * Check that the provided vector is an array with the right length
+   * @param {Matrix} matrix
+   * @param {Array|Matrix} vector
+   * @return {Array}
+   * @throws {RangeError}
+   */
+  function checkRowVector(matrix, vector) {
+    if (vector.to1DArray) {
+      vector = vector.to1DArray();
+    }
+    if (vector.length !== matrix.columns) {
+      throw new RangeError(
+        'vector size must be the same as the number of columns',
+      );
+    }
+    return vector;
+  }
+  
+  /**
+   * @private
+   * Check that the provided vector is an array with the right length
+   * @param {Matrix} matrix
+   * @param {Array|Matrix} vector
+   * @return {Array}
+   * @throws {RangeError}
+   */
+  function checkColumnVector(matrix, vector) {
+    if (vector.to1DArray) {
+      vector = vector.to1DArray();
+    }
+    if (vector.length !== matrix.rows) {
+      throw new RangeError('vector size must be the same as the number of rows');
+    }
+    return vector;
+  }
+  
+  function checkRowIndices(matrix, rowIndices) {
+    if (!isAnyArray.isAnyArray(rowIndices)) {
+      throw new TypeError('row indices must be an array');
+    }
+  
+    for (let i = 0; i < rowIndices.length; i++) {
+      if (rowIndices[i] < 0 || rowIndices[i] >= matrix.rows) {
+        throw new RangeError('row indices are out of range');
+      }
+    }
+  }
+  
+  function checkColumnIndices(matrix, columnIndices) {
+    if (!isAnyArray.isAnyArray(columnIndices)) {
+      throw new TypeError('column indices must be an array');
+    }
+  
+    for (let i = 0; i < columnIndices.length; i++) {
+      if (columnIndices[i] < 0 || columnIndices[i] >= matrix.columns) {
+        throw new RangeError('column indices are out of range');
+      }
+    }
+  }
+  
+  function checkRange(matrix, startRow, endRow, startColumn, endColumn) {
+    if (arguments.length !== 5) {
+      throw new RangeError('expected 4 arguments');
+    }
+    checkNumber('startRow', startRow);
+    checkNumber('endRow', endRow);
+    checkNumber('startColumn', startColumn);
+    checkNumber('endColumn', endColumn);
+    if (
+      startRow > endRow ||
+      startColumn > endColumn ||
+      startRow < 0 ||
+      startRow >= matrix.rows ||
+      endRow < 0 ||
+      endRow >= matrix.rows ||
+      startColumn < 0 ||
+      startColumn >= matrix.columns ||
+      endColumn < 0 ||
+      endColumn >= matrix.columns
+    ) {
+      throw new RangeError('Submatrix indices are out of range');
+    }
+  }
+  
+  function newArray(length, value = 0) {
+    let array = [];
+    for (let i = 0; i < length; i++) {
+      array.push(value);
+    }
+    return array;
+  }
+  
+  function checkNumber(name, value) {
+    if (typeof value !== 'number') {
+      throw new TypeError(`${name} must be a number`);
+    }
+  }
+  
+  function checkNonEmpty(matrix) {
+    if (matrix.isEmpty()) {
+      throw new Error('Empty matrix has no elements to index');
+    }
+  }
+  
+  function sumByRow(matrix) {
+    let sum = newArray(matrix.rows);
+    for (let i = 0; i < matrix.rows; ++i) {
+      for (let j = 0; j < matrix.columns; ++j) {
+        sum[i] += matrix.get(i, j);
+      }
+    }
+    return sum;
+  }
+  
+  function sumByColumn(matrix) {
+    let sum = newArray(matrix.columns);
+    for (let i = 0; i < matrix.rows; ++i) {
+      for (let j = 0; j < matrix.columns; ++j) {
+        sum[j] += matrix.get(i, j);
+      }
+    }
+    return sum;
+  }
+  
+  function sumAll(matrix) {
+    let v = 0;
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        v += matrix.get(i, j);
+      }
+    }
+    return v;
+  }
+  
+  function productByRow(matrix) {
+    let sum = newArray(matrix.rows, 1);
+    for (let i = 0; i < matrix.rows; ++i) {
+      for (let j = 0; j < matrix.columns; ++j) {
+        sum[i] *= matrix.get(i, j);
+      }
+    }
+    return sum;
+  }
+  
+  function productByColumn(matrix) {
+    let sum = newArray(matrix.columns, 1);
+    for (let i = 0; i < matrix.rows; ++i) {
+      for (let j = 0; j < matrix.columns; ++j) {
+        sum[j] *= matrix.get(i, j);
+      }
+    }
+    return sum;
+  }
+  
+  function productAll(matrix) {
+    let v = 1;
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        v *= matrix.get(i, j);
+      }
+    }
+    return v;
+  }
+  
+  function varianceByRow(matrix, unbiased, mean) {
+    const rows = matrix.rows;
+    const cols = matrix.columns;
+    const variance = [];
+  
+    for (let i = 0; i < rows; i++) {
+      let sum1 = 0;
+      let sum2 = 0;
+      let x = 0;
+      for (let j = 0; j < cols; j++) {
+        x = matrix.get(i, j) - mean[i];
+        sum1 += x;
+        sum2 += x * x;
+      }
+      if (unbiased) {
+        variance.push((sum2 - (sum1 * sum1) / cols) / (cols - 1));
+      } else {
+        variance.push((sum2 - (sum1 * sum1) / cols) / cols);
+      }
+    }
+    return variance;
+  }
+  
+  function varianceByColumn(matrix, unbiased, mean) {
+    const rows = matrix.rows;
+    const cols = matrix.columns;
+    const variance = [];
+  
+    for (let j = 0; j < cols; j++) {
+      let sum1 = 0;
+      let sum2 = 0;
+      let x = 0;
+      for (let i = 0; i < rows; i++) {
+        x = matrix.get(i, j) - mean[j];
+        sum1 += x;
+        sum2 += x * x;
+      }
+      if (unbiased) {
+        variance.push((sum2 - (sum1 * sum1) / rows) / (rows - 1));
+      } else {
+        variance.push((sum2 - (sum1 * sum1) / rows) / rows);
+      }
+    }
+    return variance;
+  }
+  
+  function varianceAll(matrix, unbiased, mean) {
+    const rows = matrix.rows;
+    const cols = matrix.columns;
+    const size = rows * cols;
+  
+    let sum1 = 0;
+    let sum2 = 0;
+    let x = 0;
+    for (let i = 0; i < rows; i++) {
+      for (let j = 0; j < cols; j++) {
+        x = matrix.get(i, j) - mean;
+        sum1 += x;
+        sum2 += x * x;
+      }
+    }
+    if (unbiased) {
+      return (sum2 - (sum1 * sum1) / size) / (size - 1);
+    } else {
+      return (sum2 - (sum1 * sum1) / size) / size;
+    }
+  }
+  
+  function centerByRow(matrix, mean) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) - mean[i]);
+      }
+    }
+  }
+  
+  function centerByColumn(matrix, mean) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) - mean[j]);
+      }
+    }
+  }
+  
+  function centerAll(matrix, mean) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) - mean);
+      }
+    }
+  }
+  
+  function getScaleByRow(matrix) {
+    const scale = [];
+    for (let i = 0; i < matrix.rows; i++) {
+      let sum = 0;
+      for (let j = 0; j < matrix.columns; j++) {
+        sum += Math.pow(matrix.get(i, j), 2) / (matrix.columns - 1);
+      }
+      scale.push(Math.sqrt(sum));
+    }
+    return scale;
+  }
+  
+  function scaleByRow(matrix, scale) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) / scale[i]);
+      }
+    }
+  }
+  
+  function getScaleByColumn(matrix) {
+    const scale = [];
+    for (let j = 0; j < matrix.columns; j++) {
+      let sum = 0;
+      for (let i = 0; i < matrix.rows; i++) {
+        sum += Math.pow(matrix.get(i, j), 2) / (matrix.rows - 1);
+      }
+      scale.push(Math.sqrt(sum));
+    }
+    return scale;
+  }
+  
+  function scaleByColumn(matrix, scale) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) / scale[j]);
+      }
+    }
+  }
+  
+  function getScaleAll(matrix) {
+    const divider = matrix.size - 1;
+    let sum = 0;
+    for (let j = 0; j < matrix.columns; j++) {
+      for (let i = 0; i < matrix.rows; i++) {
+        sum += Math.pow(matrix.get(i, j), 2) / divider;
+      }
+    }
+    return Math.sqrt(sum);
+  }
+  
+  function scaleAll(matrix, scale) {
+    for (let i = 0; i < matrix.rows; i++) {
+      for (let j = 0; j < matrix.columns; j++) {
+        matrix.set(i, j, matrix.get(i, j) / scale);
+      }
+    }
+  }
+  
+  class AbstractMatrix {
+    static from1DArray(newRows, newColumns, newData) {
+      let length = newRows * newColumns;
+      if (length !== newData.length) {
+        throw new RangeError('data length does not match given dimensions');
+      }
+      let newMatrix = new Matrix(newRows, newColumns);
+      for (let row = 0; row < newRows; row++) {
+        for (let column = 0; column < newColumns; column++) {
+          newMatrix.set(row, column, newData[row * newColumns + column]);
+        }
+      }
+      return newMatrix;
+    }
+  
+    static rowVector(newData) {
+      let vector = new Matrix(1, newData.length);
+      for (let i = 0; i < newData.length; i++) {
+        vector.set(0, i, newData[i]);
+      }
+      return vector;
+    }
+  
+    static columnVector(newData) {
+      let vector = new Matrix(newData.length, 1);
+      for (let i = 0; i < newData.length; i++) {
+        vector.set(i, 0, newData[i]);
+      }
+      return vector;
+    }
+  
+    static zeros(rows, columns) {
+      return new Matrix(rows, columns);
+    }
+  
+    static ones(rows, columns) {
+      return new Matrix(rows, columns).fill(1);
+    }
+  
+    static rand(rows, columns, options = {}) {
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { random = Math.random } = options;
+      let matrix = new Matrix(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          matrix.set(i, j, random());
+        }
+      }
+      return matrix;
+    }
+  
+    static randInt(rows, columns, options = {}) {
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { min = 0, max = 1000, random = Math.random } = options;
+      if (!Number.isInteger(min)) throw new TypeError('min must be an integer');
+      if (!Number.isInteger(max)) throw new TypeError('max must be an integer');
+      if (min >= max) throw new RangeError('min must be smaller than max');
+      let interval = max - min;
+      let matrix = new Matrix(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          let value = min + Math.round(random() * interval);
+          matrix.set(i, j, value);
+        }
+      }
+      return matrix;
+    }
+  
+    static eye(rows, columns, value) {
+      if (columns === undefined) columns = rows;
+      if (value === undefined) value = 1;
+      let min = Math.min(rows, columns);
+      let matrix = this.zeros(rows, columns);
+      for (let i = 0; i < min; i++) {
+        matrix.set(i, i, value);
+      }
+      return matrix;
+    }
+  
+    static diag(data, rows, columns) {
+      let l = data.length;
+      if (rows === undefined) rows = l;
+      if (columns === undefined) columns = rows;
+      let min = Math.min(l, rows, columns);
+      let matrix = this.zeros(rows, columns);
+      for (let i = 0; i < min; i++) {
+        matrix.set(i, i, data[i]);
+      }
+      return matrix;
+    }
+  
+    static min(matrix1, matrix2) {
+      matrix1 = this.checkMatrix(matrix1);
+      matrix2 = this.checkMatrix(matrix2);
+      let rows = matrix1.rows;
+      let columns = matrix1.columns;
+      let result = new Matrix(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          result.set(i, j, Math.min(matrix1.get(i, j), matrix2.get(i, j)));
+        }
+      }
+      return result;
+    }
+  
+    static max(matrix1, matrix2) {
+      matrix1 = this.checkMatrix(matrix1);
+      matrix2 = this.checkMatrix(matrix2);
+      let rows = matrix1.rows;
+      let columns = matrix1.columns;
+      let result = new this(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          result.set(i, j, Math.max(matrix1.get(i, j), matrix2.get(i, j)));
+        }
+      }
+      return result;
+    }
+  
+    static checkMatrix(value) {
+      return AbstractMatrix.isMatrix(value) ? value : new Matrix(value);
+    }
+  
+    static isMatrix(value) {
+      return value != null && value.klass === 'Matrix';
+    }
+  
+    get size() {
+      return this.rows * this.columns;
+    }
+  
+    apply(callback) {
+      if (typeof callback !== 'function') {
+        throw new TypeError('callback must be a function');
+      }
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          callback.call(this, i, j);
+        }
+      }
+      return this;
+    }
+  
+    to1DArray() {
+      let array = [];
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          array.push(this.get(i, j));
+        }
+      }
+      return array;
+    }
+  
+    to2DArray() {
+      let copy = [];
+      for (let i = 0; i < this.rows; i++) {
+        copy.push([]);
+        for (let j = 0; j < this.columns; j++) {
+          copy[i].push(this.get(i, j));
+        }
+      }
+      return copy;
+    }
+  
+    toJSON() {
+      return this.to2DArray();
+    }
+  
+    isRowVector() {
+      return this.rows === 1;
+    }
+  
+    isColumnVector() {
+      return this.columns === 1;
+    }
+  
+    isVector() {
+      return this.rows === 1 || this.columns === 1;
+    }
+  
+    isSquare() {
+      return this.rows === this.columns;
+    }
+  
+    isEmpty() {
+      return this.rows === 0 || this.columns === 0;
+    }
+  
+    isSymmetric() {
+      if (this.isSquare()) {
+        for (let i = 0; i < this.rows; i++) {
+          for (let j = 0; j <= i; j++) {
+            if (this.get(i, j) !== this.get(j, i)) {
+              return false;
+            }
+          }
+        }
+        return true;
+      }
+      return false;
+    }
+  
+    isEchelonForm() {
+      let i = 0;
+      let j = 0;
+      let previousColumn = -1;
+      let isEchelonForm = true;
+      let checked = false;
+      while (i < this.rows && isEchelonForm) {
+        j = 0;
+        checked = false;
+        while (j < this.columns && checked === false) {
+          if (this.get(i, j) === 0) {
+            j++;
+          } else if (this.get(i, j) === 1 && j > previousColumn) {
+            checked = true;
+            previousColumn = j;
+          } else {
+            isEchelonForm = false;
+            checked = true;
+          }
+        }
+        i++;
+      }
+      return isEchelonForm;
+    }
+  
+    isReducedEchelonForm() {
+      let i = 0;
+      let j = 0;
+      let previousColumn = -1;
+      let isReducedEchelonForm = true;
+      let checked = false;
+      while (i < this.rows && isReducedEchelonForm) {
+        j = 0;
+        checked = false;
+        while (j < this.columns && checked === false) {
+          if (this.get(i, j) === 0) {
+            j++;
+          } else if (this.get(i, j) === 1 && j > previousColumn) {
+            checked = true;
+            previousColumn = j;
+          } else {
+            isReducedEchelonForm = false;
+            checked = true;
+          }
+        }
+        for (let k = j + 1; k < this.rows; k++) {
+          if (this.get(i, k) !== 0) {
+            isReducedEchelonForm = false;
+          }
+        }
+        i++;
+      }
+      return isReducedEchelonForm;
+    }
+  
+    echelonForm() {
+      let result = this.clone();
+      let h = 0;
+      let k = 0;
+      while (h < result.rows && k < result.columns) {
+        let iMax = h;
+        for (let i = h; i < result.rows; i++) {
+          if (result.get(i, k) > result.get(iMax, k)) {
+            iMax = i;
+          }
+        }
+        if (result.get(iMax, k) === 0) {
+          k++;
+        } else {
+          result.swapRows(h, iMax);
+          let tmp = result.get(h, k);
+          for (let j = k; j < result.columns; j++) {
+            result.set(h, j, result.get(h, j) / tmp);
+          }
+          for (let i = h + 1; i < result.rows; i++) {
+            let factor = result.get(i, k) / result.get(h, k);
+            result.set(i, k, 0);
+            for (let j = k + 1; j < result.columns; j++) {
+              result.set(i, j, result.get(i, j) - result.get(h, j) * factor);
+            }
+          }
+          h++;
+          k++;
+        }
+      }
+      return result;
+    }
+  
+    reducedEchelonForm() {
+      let result = this.echelonForm();
+      let m = result.columns;
+      let n = result.rows;
+      let h = n - 1;
+      while (h >= 0) {
+        if (result.maxRow(h) === 0) {
+          h--;
+        } else {
+          let p = 0;
+          let pivot = false;
+          while (p < n && pivot === false) {
+            if (result.get(h, p) === 1) {
+              pivot = true;
+            } else {
+              p++;
+            }
+          }
+          for (let i = 0; i < h; i++) {
+            let factor = result.get(i, p);
+            for (let j = p; j < m; j++) {
+              let tmp = result.get(i, j) - factor * result.get(h, j);
+              result.set(i, j, tmp);
+            }
+          }
+          h--;
+        }
+      }
+      return result;
+    }
+  
+    set() {
+      throw new Error('set method is unimplemented');
+    }
+  
+    get() {
+      throw new Error('get method is unimplemented');
+    }
+  
+    repeat(options = {}) {
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { rows = 1, columns = 1 } = options;
+      if (!Number.isInteger(rows) || rows <= 0) {
+        throw new TypeError('rows must be a positive integer');
+      }
+      if (!Number.isInteger(columns) || columns <= 0) {
+        throw new TypeError('columns must be a positive integer');
+      }
+      let matrix = new Matrix(this.rows * rows, this.columns * columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          matrix.setSubMatrix(this, this.rows * i, this.columns * j);
+        }
+      }
+      return matrix;
+    }
+  
+    fill(value) {
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, value);
+        }
+      }
+      return this;
+    }
+  
+    neg() {
+      return this.mulS(-1);
+    }
+  
+    getRow(index) {
+      checkRowIndex(this, index);
+      let row = [];
+      for (let i = 0; i < this.columns; i++) {
+        row.push(this.get(index, i));
+      }
+      return row;
+    }
+  
+    getRowVector(index) {
+      return Matrix.rowVector(this.getRow(index));
+    }
+  
+    setRow(index, array) {
+      checkRowIndex(this, index);
+      array = checkRowVector(this, array);
+      for (let i = 0; i < this.columns; i++) {
+        this.set(index, i, array[i]);
+      }
+      return this;
+    }
+  
+    swapRows(row1, row2) {
+      checkRowIndex(this, row1);
+      checkRowIndex(this, row2);
+      for (let i = 0; i < this.columns; i++) {
+        let temp = this.get(row1, i);
+        this.set(row1, i, this.get(row2, i));
+        this.set(row2, i, temp);
+      }
+      return this;
+    }
+  
+    getColumn(index) {
+      checkColumnIndex(this, index);
+      let column = [];
+      for (let i = 0; i < this.rows; i++) {
+        column.push(this.get(i, index));
+      }
+      return column;
+    }
+  
+    getColumnVector(index) {
+      return Matrix.columnVector(this.getColumn(index));
+    }
+  
+    setColumn(index, array) {
+      checkColumnIndex(this, index);
+      array = checkColumnVector(this, array);
+      for (let i = 0; i < this.rows; i++) {
+        this.set(i, index, array[i]);
+      }
+      return this;
+    }
+  
+    swapColumns(column1, column2) {
+      checkColumnIndex(this, column1);
+      checkColumnIndex(this, column2);
+      for (let i = 0; i < this.rows; i++) {
+        let temp = this.get(i, column1);
+        this.set(i, column1, this.get(i, column2));
+        this.set(i, column2, temp);
+      }
+      return this;
+    }
+  
+    addRowVector(vector) {
+      vector = checkRowVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) + vector[j]);
+        }
+      }
+      return this;
+    }
+  
+    subRowVector(vector) {
+      vector = checkRowVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) - vector[j]);
+        }
+      }
+      return this;
+    }
+  
+    mulRowVector(vector) {
+      vector = checkRowVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) * vector[j]);
+        }
+      }
+      return this;
+    }
+  
+    divRowVector(vector) {
+      vector = checkRowVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) / vector[j]);
+        }
+      }
+      return this;
+    }
+  
+    addColumnVector(vector) {
+      vector = checkColumnVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) + vector[i]);
+        }
+      }
+      return this;
+    }
+  
+    subColumnVector(vector) {
+      vector = checkColumnVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) - vector[i]);
+        }
+      }
+      return this;
+    }
+  
+    mulColumnVector(vector) {
+      vector = checkColumnVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) * vector[i]);
+        }
+      }
+      return this;
+    }
+  
+    divColumnVector(vector) {
+      vector = checkColumnVector(this, vector);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          this.set(i, j, this.get(i, j) / vector[i]);
+        }
+      }
+      return this;
+    }
+  
+    mulRow(index, value) {
+      checkRowIndex(this, index);
+      for (let i = 0; i < this.columns; i++) {
+        this.set(index, i, this.get(index, i) * value);
+      }
+      return this;
+    }
+  
+    mulColumn(index, value) {
+      checkColumnIndex(this, index);
+      for (let i = 0; i < this.rows; i++) {
+        this.set(i, index, this.get(i, index) * value);
+      }
+      return this;
+    }
+  
+    max(by) {
+      if (this.isEmpty()) {
+        return NaN;
+      }
+      switch (by) {
+        case 'row': {
+          const max = new Array(this.rows).fill(Number.NEGATIVE_INFINITY);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) > max[row]) {
+                max[row] = this.get(row, column);
+              }
+            }
+          }
+          return max;
+        }
+        case 'column': {
+          const max = new Array(this.columns).fill(Number.NEGATIVE_INFINITY);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) > max[column]) {
+                max[column] = this.get(row, column);
+              }
+            }
+          }
+          return max;
+        }
+        case undefined: {
+          let max = this.get(0, 0);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) > max) {
+                max = this.get(row, column);
+              }
+            }
+          }
+          return max;
+        }
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    maxIndex() {
+      checkNonEmpty(this);
+      let v = this.get(0, 0);
+      let idx = [0, 0];
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          if (this.get(i, j) > v) {
+            v = this.get(i, j);
+            idx[0] = i;
+            idx[1] = j;
+          }
+        }
+      }
+      return idx;
+    }
+  
+    min(by) {
+      if (this.isEmpty()) {
+        return NaN;
+      }
+  
+      switch (by) {
+        case 'row': {
+          const min = new Array(this.rows).fill(Number.POSITIVE_INFINITY);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) < min[row]) {
+                min[row] = this.get(row, column);
+              }
+            }
+          }
+          return min;
+        }
+        case 'column': {
+          const min = new Array(this.columns).fill(Number.POSITIVE_INFINITY);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) < min[column]) {
+                min[column] = this.get(row, column);
+              }
+            }
+          }
+          return min;
+        }
+        case undefined: {
+          let min = this.get(0, 0);
+          for (let row = 0; row < this.rows; row++) {
+            for (let column = 0; column < this.columns; column++) {
+              if (this.get(row, column) < min) {
+                min = this.get(row, column);
+              }
+            }
+          }
+          return min;
+        }
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    minIndex() {
+      checkNonEmpty(this);
+      let v = this.get(0, 0);
+      let idx = [0, 0];
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          if (this.get(i, j) < v) {
+            v = this.get(i, j);
+            idx[0] = i;
+            idx[1] = j;
+          }
+        }
+      }
+      return idx;
+    }
+  
+    maxRow(row) {
+      checkRowIndex(this, row);
+      if (this.isEmpty()) {
+        return NaN;
+      }
+      let v = this.get(row, 0);
+      for (let i = 1; i < this.columns; i++) {
+        if (this.get(row, i) > v) {
+          v = this.get(row, i);
+        }
+      }
+      return v;
+    }
+  
+    maxRowIndex(row) {
+      checkRowIndex(this, row);
+      checkNonEmpty(this);
+      let v = this.get(row, 0);
+      let idx = [row, 0];
+      for (let i = 1; i < this.columns; i++) {
+        if (this.get(row, i) > v) {
+          v = this.get(row, i);
+          idx[1] = i;
+        }
+      }
+      return idx;
+    }
+  
+    minRow(row) {
+      checkRowIndex(this, row);
+      if (this.isEmpty()) {
+        return NaN;
+      }
+      let v = this.get(row, 0);
+      for (let i = 1; i < this.columns; i++) {
+        if (this.get(row, i) < v) {
+          v = this.get(row, i);
+        }
+      }
+      return v;
+    }
+  
+    minRowIndex(row) {
+      checkRowIndex(this, row);
+      checkNonEmpty(this);
+      let v = this.get(row, 0);
+      let idx = [row, 0];
+      for (let i = 1; i < this.columns; i++) {
+        if (this.get(row, i) < v) {
+          v = this.get(row, i);
+          idx[1] = i;
+        }
+      }
+      return idx;
+    }
+  
+    maxColumn(column) {
+      checkColumnIndex(this, column);
+      if (this.isEmpty()) {
+        return NaN;
+      }
+      let v = this.get(0, column);
+      for (let i = 1; i < this.rows; i++) {
+        if (this.get(i, column) > v) {
+          v = this.get(i, column);
+        }
+      }
+      return v;
+    }
+  
+    maxColumnIndex(column) {
+      checkColumnIndex(this, column);
+      checkNonEmpty(this);
+      let v = this.get(0, column);
+      let idx = [0, column];
+      for (let i = 1; i < this.rows; i++) {
+        if (this.get(i, column) > v) {
+          v = this.get(i, column);
+          idx[0] = i;
+        }
+      }
+      return idx;
+    }
+  
+    minColumn(column) {
+      checkColumnIndex(this, column);
+      if (this.isEmpty()) {
+        return NaN;
+      }
+      let v = this.get(0, column);
+      for (let i = 1; i < this.rows; i++) {
+        if (this.get(i, column) < v) {
+          v = this.get(i, column);
+        }
+      }
+      return v;
+    }
+  
+    minColumnIndex(column) {
+      checkColumnIndex(this, column);
+      checkNonEmpty(this);
+      let v = this.get(0, column);
+      let idx = [0, column];
+      for (let i = 1; i < this.rows; i++) {
+        if (this.get(i, column) < v) {
+          v = this.get(i, column);
+          idx[0] = i;
+        }
+      }
+      return idx;
+    }
+  
+    diag() {
+      let min = Math.min(this.rows, this.columns);
+      let diag = [];
+      for (let i = 0; i < min; i++) {
+        diag.push(this.get(i, i));
+      }
+      return diag;
+    }
+  
+    norm(type = 'frobenius') {
+      let result = 0;
+      if (type === 'max') {
+        return this.max();
+      } else if (type === 'frobenius') {
+        for (let i = 0; i < this.rows; i++) {
+          for (let j = 0; j < this.columns; j++) {
+            result = result + this.get(i, j) * this.get(i, j);
+          }
+        }
+        return Math.sqrt(result);
+      } else {
+        throw new RangeError(`unknown norm type: ${type}`);
+      }
+    }
+  
+    cumulativeSum() {
+      let sum = 0;
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          sum += this.get(i, j);
+          this.set(i, j, sum);
+        }
+      }
+      return this;
+    }
+  
+    dot(vector2) {
+      if (AbstractMatrix.isMatrix(vector2)) vector2 = vector2.to1DArray();
+      let vector1 = this.to1DArray();
+      if (vector1.length !== vector2.length) {
+        throw new RangeError('vectors do not have the same size');
+      }
+      let dot = 0;
+      for (let i = 0; i < vector1.length; i++) {
+        dot += vector1[i] * vector2[i];
+      }
+      return dot;
+    }
+  
+    mmul(other) {
+      other = Matrix.checkMatrix(other);
+  
+      let m = this.rows;
+      let n = this.columns;
+      let p = other.columns;
+  
+      let result = new Matrix(m, p);
+  
+      let Bcolj = new Float64Array(n);
+      for (let j = 0; j < p; j++) {
+        for (let k = 0; k < n; k++) {
+          Bcolj[k] = other.get(k, j);
+        }
+  
+        for (let i = 0; i < m; i++) {
+          let s = 0;
+          for (let k = 0; k < n; k++) {
+            s += this.get(i, k) * Bcolj[k];
+          }
+  
+          result.set(i, j, s);
+        }
+      }
+      return result;
+    }
+  
+    strassen2x2(other) {
+      other = Matrix.checkMatrix(other);
+      let result = new Matrix(2, 2);
+      const a11 = this.get(0, 0);
+      const b11 = other.get(0, 0);
+      const a12 = this.get(0, 1);
+      const b12 = other.get(0, 1);
+      const a21 = this.get(1, 0);
+      const b21 = other.get(1, 0);
+      const a22 = this.get(1, 1);
+      const b22 = other.get(1, 1);
+  
+      // Compute intermediate values.
+      const m1 = (a11 + a22) * (b11 + b22);
+      const m2 = (a21 + a22) * b11;
+      const m3 = a11 * (b12 - b22);
+      const m4 = a22 * (b21 - b11);
+      const m5 = (a11 + a12) * b22;
+      const m6 = (a21 - a11) * (b11 + b12);
+      const m7 = (a12 - a22) * (b21 + b22);
+  
+      // Combine intermediate values into the output.
+      const c00 = m1 + m4 - m5 + m7;
+      const c01 = m3 + m5;
+      const c10 = m2 + m4;
+      const c11 = m1 - m2 + m3 + m6;
+  
+      result.set(0, 0, c00);
+      result.set(0, 1, c01);
+      result.set(1, 0, c10);
+      result.set(1, 1, c11);
+      return result;
+    }
+  
+    strassen3x3(other) {
+      other = Matrix.checkMatrix(other);
+      let result = new Matrix(3, 3);
+  
+      const a00 = this.get(0, 0);
+      const a01 = this.get(0, 1);
+      const a02 = this.get(0, 2);
+      const a10 = this.get(1, 0);
+      const a11 = this.get(1, 1);
+      const a12 = this.get(1, 2);
+      const a20 = this.get(2, 0);
+      const a21 = this.get(2, 1);
+      const a22 = this.get(2, 2);
+  
+      const b00 = other.get(0, 0);
+      const b01 = other.get(0, 1);
+      const b02 = other.get(0, 2);
+      const b10 = other.get(1, 0);
+      const b11 = other.get(1, 1);
+      const b12 = other.get(1, 2);
+      const b20 = other.get(2, 0);
+      const b21 = other.get(2, 1);
+      const b22 = other.get(2, 2);
+  
+      const m1 = (a00 + a01 + a02 - a10 - a11 - a21 - a22) * b11;
+      const m2 = (a00 - a10) * (-b01 + b11);
+      const m3 = a11 * (-b00 + b01 + b10 - b11 - b12 - b20 + b22);
+      const m4 = (-a00 + a10 + a11) * (b00 - b01 + b11);
+      const m5 = (a10 + a11) * (-b00 + b01);
+      const m6 = a00 * b00;
+      const m7 = (-a00 + a20 + a21) * (b00 - b02 + b12);
+      const m8 = (-a00 + a20) * (b02 - b12);
+      const m9 = (a20 + a21) * (-b00 + b02);
+      const m10 = (a00 + a01 + a02 - a11 - a12 - a20 - a21) * b12;
+      const m11 = a21 * (-b00 + b02 + b10 - b11 - b12 - b20 + b21);
+      const m12 = (-a02 + a21 + a22) * (b11 + b20 - b21);
+      const m13 = (a02 - a22) * (b11 - b21);
+      const m14 = a02 * b20;
+      const m15 = (a21 + a22) * (-b20 + b21);
+      const m16 = (-a02 + a11 + a12) * (b12 + b20 - b22);
+      const m17 = (a02 - a12) * (b12 - b22);
+      const m18 = (a11 + a12) * (-b20 + b22);
+      const m19 = a01 * b10;
+      const m20 = a12 * b21;
+      const m21 = a10 * b02;
+      const m22 = a20 * b01;
+      const m23 = a22 * b22;
+  
+      const c00 = m6 + m14 + m19;
+      const c01 = m1 + m4 + m5 + m6 + m12 + m14 + m15;
+      const c02 = m6 + m7 + m9 + m10 + m14 + m16 + m18;
+      const c10 = m2 + m3 + m4 + m6 + m14 + m16 + m17;
+      const c11 = m2 + m4 + m5 + m6 + m20;
+      const c12 = m14 + m16 + m17 + m18 + m21;
+      const c20 = m6 + m7 + m8 + m11 + m12 + m13 + m14;
+      const c21 = m12 + m13 + m14 + m15 + m22;
+      const c22 = m6 + m7 + m8 + m9 + m23;
+  
+      result.set(0, 0, c00);
+      result.set(0, 1, c01);
+      result.set(0, 2, c02);
+      result.set(1, 0, c10);
+      result.set(1, 1, c11);
+      result.set(1, 2, c12);
+      result.set(2, 0, c20);
+      result.set(2, 1, c21);
+      result.set(2, 2, c22);
+      return result;
+    }
+  
+    mmulStrassen(y) {
+      y = Matrix.checkMatrix(y);
+      let x = this.clone();
+      let r1 = x.rows;
+      let c1 = x.columns;
+      let r2 = y.rows;
+      let c2 = y.columns;
+      if (c1 !== r2) {
+        // eslint-disable-next-line no-console
+        console.warn(
+          `Multiplying ${r1} x ${c1} and ${r2} x ${c2} matrix: dimensions do not match.`,
+        );
+      }
+  
+      // Put a matrix into the top left of a matrix of zeros.
+      // `rows` and `cols` are the dimensions of the output matrix.
+      function embed(mat, rows, cols) {
+        let r = mat.rows;
+        let c = mat.columns;
+        if (r === rows && c === cols) {
+          return mat;
+        } else {
+          let resultat = AbstractMatrix.zeros(rows, cols);
+          resultat = resultat.setSubMatrix(mat, 0, 0);
+          return resultat;
+        }
+      }
+  
+      // Make sure both matrices are the same size.
+      // This is exclusively for simplicity:
+      // this algorithm can be implemented with matrices of different sizes.
+  
+      let r = Math.max(r1, r2);
+      let c = Math.max(c1, c2);
+      x = embed(x, r, c);
+      y = embed(y, r, c);
+  
+      // Our recursive multiplication function.
+      function blockMult(a, b, rows, cols) {
+        // For small matrices, resort to naive multiplication.
+        if (rows <= 512 || cols <= 512) {
+          return a.mmul(b); // a is equivalent to this
+        }
+  
+        // Apply dynamic padding.
+        if (rows % 2 === 1 && cols % 2 === 1) {
+          a = embed(a, rows + 1, cols + 1);
+          b = embed(b, rows + 1, cols + 1);
+        } else if (rows % 2 === 1) {
+          a = embed(a, rows + 1, cols);
+          b = embed(b, rows + 1, cols);
+        } else if (cols % 2 === 1) {
+          a = embed(a, rows, cols + 1);
+          b = embed(b, rows, cols + 1);
+        }
+  
+        let halfRows = parseInt(a.rows / 2, 10);
+        let halfCols = parseInt(a.columns / 2, 10);
+        // Subdivide input matrices.
+        let a11 = a.subMatrix(0, halfRows - 1, 0, halfCols - 1);
+        let b11 = b.subMatrix(0, halfRows - 1, 0, halfCols - 1);
+  
+        let a12 = a.subMatrix(0, halfRows - 1, halfCols, a.columns - 1);
+        let b12 = b.subMatrix(0, halfRows - 1, halfCols, b.columns - 1);
+  
+        let a21 = a.subMatrix(halfRows, a.rows - 1, 0, halfCols - 1);
+        let b21 = b.subMatrix(halfRows, b.rows - 1, 0, halfCols - 1);
+  
+        let a22 = a.subMatrix(halfRows, a.rows - 1, halfCols, a.columns - 1);
+        let b22 = b.subMatrix(halfRows, b.rows - 1, halfCols, b.columns - 1);
+  
+        // Compute intermediate values.
+        let m1 = blockMult(
+          AbstractMatrix.add(a11, a22),
+          AbstractMatrix.add(b11, b22),
+          halfRows,
+          halfCols,
+        );
+        let m2 = blockMult(AbstractMatrix.add(a21, a22), b11, halfRows, halfCols);
+        let m3 = blockMult(a11, AbstractMatrix.sub(b12, b22), halfRows, halfCols);
+        let m4 = blockMult(a22, AbstractMatrix.sub(b21, b11), halfRows, halfCols);
+        let m5 = blockMult(AbstractMatrix.add(a11, a12), b22, halfRows, halfCols);
+        let m6 = blockMult(
+          AbstractMatrix.sub(a21, a11),
+          AbstractMatrix.add(b11, b12),
+          halfRows,
+          halfCols,
+        );
+        let m7 = blockMult(
+          AbstractMatrix.sub(a12, a22),
+          AbstractMatrix.add(b21, b22),
+          halfRows,
+          halfCols,
+        );
+  
+        // Combine intermediate values into the output.
+        let c11 = AbstractMatrix.add(m1, m4);
+        c11.sub(m5);
+        c11.add(m7);
+        let c12 = AbstractMatrix.add(m3, m5);
+        let c21 = AbstractMatrix.add(m2, m4);
+        let c22 = AbstractMatrix.sub(m1, m2);
+        c22.add(m3);
+        c22.add(m6);
+  
+        // Crop output to the desired size (undo dynamic padding).
+        let resultat = AbstractMatrix.zeros(2 * c11.rows, 2 * c11.columns);
+        resultat = resultat.setSubMatrix(c11, 0, 0);
+        resultat = resultat.setSubMatrix(c12, c11.rows, 0);
+        resultat = resultat.setSubMatrix(c21, 0, c11.columns);
+        resultat = resultat.setSubMatrix(c22, c11.rows, c11.columns);
+        return resultat.subMatrix(0, rows - 1, 0, cols - 1);
+      }
+  
+      return blockMult(x, y, r, c);
+    }
+  
+    scaleRows(options = {}) {
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { min = 0, max = 1 } = options;
+      if (!Number.isFinite(min)) throw new TypeError('min must be a number');
+      if (!Number.isFinite(max)) throw new TypeError('max must be a number');
+      if (min >= max) throw new RangeError('min must be smaller than max');
+      let newMatrix = new Matrix(this.rows, this.columns);
+      for (let i = 0; i < this.rows; i++) {
+        const row = this.getRow(i);
+        if (row.length > 0) {
+          rescale__default["default"](row, { min, max, output: row });
+        }
+        newMatrix.setRow(i, row);
+      }
+      return newMatrix;
+    }
+  
+    scaleColumns(options = {}) {
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { min = 0, max = 1 } = options;
+      if (!Number.isFinite(min)) throw new TypeError('min must be a number');
+      if (!Number.isFinite(max)) throw new TypeError('max must be a number');
+      if (min >= max) throw new RangeError('min must be smaller than max');
+      let newMatrix = new Matrix(this.rows, this.columns);
+      for (let i = 0; i < this.columns; i++) {
+        const column = this.getColumn(i);
+        if (column.length) {
+          rescale__default["default"](column, {
+            min: min,
+            max: max,
+            output: column,
+          });
+        }
+        newMatrix.setColumn(i, column);
+      }
+      return newMatrix;
+    }
+  
+    flipRows() {
+      const middle = Math.ceil(this.columns / 2);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < middle; j++) {
+          let first = this.get(i, j);
+          let last = this.get(i, this.columns - 1 - j);
+          this.set(i, j, last);
+          this.set(i, this.columns - 1 - j, first);
+        }
+      }
+      return this;
+    }
+  
+    flipColumns() {
+      const middle = Math.ceil(this.rows / 2);
+      for (let j = 0; j < this.columns; j++) {
+        for (let i = 0; i < middle; i++) {
+          let first = this.get(i, j);
+          let last = this.get(this.rows - 1 - i, j);
+          this.set(i, j, last);
+          this.set(this.rows - 1 - i, j, first);
+        }
+      }
+      return this;
+    }
+  
+    kroneckerProduct(other) {
+      other = Matrix.checkMatrix(other);
+  
+      let m = this.rows;
+      let n = this.columns;
+      let p = other.rows;
+      let q = other.columns;
+  
+      let result = new Matrix(m * p, n * q);
+      for (let i = 0; i < m; i++) {
+        for (let j = 0; j < n; j++) {
+          for (let k = 0; k < p; k++) {
+            for (let l = 0; l < q; l++) {
+              result.set(p * i + k, q * j + l, this.get(i, j) * other.get(k, l));
+            }
+          }
+        }
+      }
+      return result;
+    }
+  
+    kroneckerSum(other) {
+      other = Matrix.checkMatrix(other);
+      if (!this.isSquare() || !other.isSquare()) {
+        throw new Error('Kronecker Sum needs two Square Matrices');
+      }
+      let m = this.rows;
+      let n = other.rows;
+      let AxI = this.kroneckerProduct(Matrix.eye(n, n));
+      let IxB = Matrix.eye(m, m).kroneckerProduct(other);
+      return AxI.add(IxB);
+    }
+  
+    transpose() {
+      let result = new Matrix(this.columns, this.rows);
+      for (let i = 0; i < this.rows; i++) {
+        for (let j = 0; j < this.columns; j++) {
+          result.set(j, i, this.get(i, j));
+        }
+      }
+      return result;
+    }
+  
+    sortRows(compareFunction = compareNumbers) {
+      for (let i = 0; i < this.rows; i++) {
+        this.setRow(i, this.getRow(i).sort(compareFunction));
+      }
+      return this;
+    }
+  
+    sortColumns(compareFunction = compareNumbers) {
+      for (let i = 0; i < this.columns; i++) {
+        this.setColumn(i, this.getColumn(i).sort(compareFunction));
+      }
+      return this;
+    }
+  
+    subMatrix(startRow, endRow, startColumn, endColumn) {
+      checkRange(this, startRow, endRow, startColumn, endColumn);
+      let newMatrix = new Matrix(
+        endRow - startRow + 1,
+        endColumn - startColumn + 1,
+      );
+      for (let i = startRow; i <= endRow; i++) {
+        for (let j = startColumn; j <= endColumn; j++) {
+          newMatrix.set(i - startRow, j - startColumn, this.get(i, j));
+        }
+      }
+      return newMatrix;
+    }
+  
+    subMatrixRow(indices, startColumn, endColumn) {
+      if (startColumn === undefined) startColumn = 0;
+      if (endColumn === undefined) endColumn = this.columns - 1;
+      if (
+        startColumn > endColumn ||
+        startColumn < 0 ||
+        startColumn >= this.columns ||
+        endColumn < 0 ||
+        endColumn >= this.columns
+      ) {
+        throw new RangeError('Argument out of range');
+      }
+  
+      let newMatrix = new Matrix(indices.length, endColumn - startColumn + 1);
+      for (let i = 0; i < indices.length; i++) {
+        for (let j = startColumn; j <= endColumn; j++) {
+          if (indices[i] < 0 || indices[i] >= this.rows) {
+            throw new RangeError(`Row index out of range: ${indices[i]}`);
+          }
+          newMatrix.set(i, j - startColumn, this.get(indices[i], j));
+        }
+      }
+      return newMatrix;
+    }
+  
+    subMatrixColumn(indices, startRow, endRow) {
+      if (startRow === undefined) startRow = 0;
+      if (endRow === undefined) endRow = this.rows - 1;
+      if (
+        startRow > endRow ||
+        startRow < 0 ||
+        startRow >= this.rows ||
+        endRow < 0 ||
+        endRow >= this.rows
+      ) {
+        throw new RangeError('Argument out of range');
+      }
+  
+      let newMatrix = new Matrix(endRow - startRow + 1, indices.length);
+      for (let i = 0; i < indices.length; i++) {
+        for (let j = startRow; j <= endRow; j++) {
+          if (indices[i] < 0 || indices[i] >= this.columns) {
+            throw new RangeError(`Column index out of range: ${indices[i]}`);
+          }
+          newMatrix.set(j - startRow, i, this.get(j, indices[i]));
+        }
+      }
+      return newMatrix;
+    }
+  
+    setSubMatrix(matrix, startRow, startColumn) {
+      matrix = Matrix.checkMatrix(matrix);
+      if (matrix.isEmpty()) {
+        return this;
+      }
+      let endRow = startRow + matrix.rows - 1;
+      let endColumn = startColumn + matrix.columns - 1;
+      checkRange(this, startRow, endRow, startColumn, endColumn);
+      for (let i = 0; i < matrix.rows; i++) {
+        for (let j = 0; j < matrix.columns; j++) {
+          this.set(startRow + i, startColumn + j, matrix.get(i, j));
+        }
+      }
+      return this;
+    }
+  
+    selection(rowIndices, columnIndices) {
+      checkRowIndices(this, rowIndices);
+      checkColumnIndices(this, columnIndices);
+      let newMatrix = new Matrix(rowIndices.length, columnIndices.length);
+      for (let i = 0; i < rowIndices.length; i++) {
+        let rowIndex = rowIndices[i];
+        for (let j = 0; j < columnIndices.length; j++) {
+          let columnIndex = columnIndices[j];
+          newMatrix.set(i, j, this.get(rowIndex, columnIndex));
+        }
+      }
+      return newMatrix;
+    }
+  
+    trace() {
+      let min = Math.min(this.rows, this.columns);
+      let trace = 0;
+      for (let i = 0; i < min; i++) {
+        trace += this.get(i, i);
+      }
+      return trace;
+    }
+  
+    clone() {
+      let newMatrix = new Matrix(this.rows, this.columns);
+      for (let row = 0; row < this.rows; row++) {
+        for (let column = 0; column < this.columns; column++) {
+          newMatrix.set(row, column, this.get(row, column));
+        }
+      }
+      return newMatrix;
+    }
+  
+    sum(by) {
+      switch (by) {
+        case 'row':
+          return sumByRow(this);
+        case 'column':
+          return sumByColumn(this);
+        case undefined:
+          return sumAll(this);
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    product(by) {
+      switch (by) {
+        case 'row':
+          return productByRow(this);
+        case 'column':
+          return productByColumn(this);
+        case undefined:
+          return productAll(this);
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    mean(by) {
+      const sum = this.sum(by);
+      switch (by) {
+        case 'row': {
+          for (let i = 0; i < this.rows; i++) {
+            sum[i] /= this.columns;
+          }
+          return sum;
+        }
+        case 'column': {
+          for (let i = 0; i < this.columns; i++) {
+            sum[i] /= this.rows;
+          }
+          return sum;
+        }
+        case undefined:
+          return sum / this.size;
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    variance(by, options = {}) {
+      if (typeof by === 'object') {
+        options = by;
+        by = undefined;
+      }
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { unbiased = true, mean = this.mean(by) } = options;
+      if (typeof unbiased !== 'boolean') {
+        throw new TypeError('unbiased must be a boolean');
+      }
+      switch (by) {
+        case 'row': {
+          if (!isAnyArray.isAnyArray(mean)) {
+            throw new TypeError('mean must be an array');
+          }
+          return varianceByRow(this, unbiased, mean);
+        }
+        case 'column': {
+          if (!isAnyArray.isAnyArray(mean)) {
+            throw new TypeError('mean must be an array');
+          }
+          return varianceByColumn(this, unbiased, mean);
+        }
+        case undefined: {
+          if (typeof mean !== 'number') {
+            throw new TypeError('mean must be a number');
+          }
+          return varianceAll(this, unbiased, mean);
+        }
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    standardDeviation(by, options) {
+      if (typeof by === 'object') {
+        options = by;
+        by = undefined;
+      }
+      const variance = this.variance(by, options);
+      if (by === undefined) {
+        return Math.sqrt(variance);
+      } else {
+        for (let i = 0; i < variance.length; i++) {
+          variance[i] = Math.sqrt(variance[i]);
+        }
+        return variance;
+      }
+    }
+  
+    center(by, options = {}) {
+      if (typeof by === 'object') {
+        options = by;
+        by = undefined;
+      }
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      const { center = this.mean(by) } = options;
+      switch (by) {
+        case 'row': {
+          if (!isAnyArray.isAnyArray(center)) {
+            throw new TypeError('center must be an array');
+          }
+          centerByRow(this, center);
+          return this;
+        }
+        case 'column': {
+          if (!isAnyArray.isAnyArray(center)) {
+            throw new TypeError('center must be an array');
+          }
+          centerByColumn(this, center);
+          return this;
+        }
+        case undefined: {
+          if (typeof center !== 'number') {
+            throw new TypeError('center must be a number');
+          }
+          centerAll(this, center);
+          return this;
+        }
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    scale(by, options = {}) {
+      if (typeof by === 'object') {
+        options = by;
+        by = undefined;
+      }
+      if (typeof options !== 'object') {
+        throw new TypeError('options must be an object');
+      }
+      let scale = options.scale;
+      switch (by) {
+        case 'row': {
+          if (scale === undefined) {
+            scale = getScaleByRow(this);
+          } else if (!isAnyArray.isAnyArray(scale)) {
+            throw new TypeError('scale must be an array');
+          }
+          scaleByRow(this, scale);
+          return this;
+        }
+        case 'column': {
+          if (scale === undefined) {
+            scale = getScaleByColumn(this);
+          } else if (!isAnyArray.isAnyArray(scale)) {
+            throw new TypeError('scale must be an array');
+          }
+          scaleByColumn(this, scale);
+          return this;
+        }
+        case undefined: {
+          if (scale === undefined) {
+            scale = getScaleAll(this);
+          } else if (typeof scale !== 'number') {
+            throw new TypeError('scale must be a number');
+          }
+          scaleAll(this, scale);
+          return this;
+        }
+        default:
+          throw new Error(`invalid option: ${by}`);
+      }
+    }
+  
+    toString(options) {
+      return inspectMatrixWithOptions(this, options);
+    }
+  }
+  
+  AbstractMatrix.prototype.klass = 'Matrix';
+  if (typeof Symbol !== 'undefined') {
+    AbstractMatrix.prototype[Symbol.for('nodejs.util.inspect.custom')] =
+      inspectMatrix;
+  }
+  
+  function compareNumbers(a, b) {
+    return a - b;
+  }
+  
+  function isArrayOfNumbers(array) {
+    return array.every((element) => {
+      return typeof element === 'number';
+    });
+  }
+  
+  // Synonyms
+  AbstractMatrix.random = AbstractMatrix.rand;
+  AbstractMatrix.randomInt = AbstractMatrix.randInt;
+  AbstractMatrix.diagonal = AbstractMatrix.diag;
+  AbstractMatrix.prototype.diagonal = AbstractMatrix.prototype.diag;
+  AbstractMatrix.identity = AbstractMatrix.eye;
+  AbstractMatrix.prototype.negate = AbstractMatrix.prototype.neg;
+  AbstractMatrix.prototype.tensorProduct =
+    AbstractMatrix.prototype.kroneckerProduct;
+  
+  class Matrix extends AbstractMatrix {
+    constructor(nRows, nColumns) {
+      super();
+      if (Matrix.isMatrix(nRows)) {
+        // eslint-disable-next-line no-constructor-return
+        return nRows.clone();
+      } else if (Number.isInteger(nRows) && nRows >= 0) {
+        // Create an empty matrix
+        this.data = [];
+        if (Number.isInteger(nColumns) && nColumns >= 0) {
+          for (let i = 0; i < nRows; i++) {
+            this.data.push(new Float64Array(nColumns));
+          }
+        } else {
+          throw new TypeError('nColumns must be a positive integer');
+        }
+      } else if (isAnyArray.isAnyArray(nRows)) {
+        // Copy the values from the 2D array
+        const arrayData = nRows;
+        nRows = arrayData.length;
+        nColumns = nRows ? arrayData[0].length : 0;
+        if (typeof nColumns !== 'number') {
+          throw new TypeError(
+            'Data must be a 2D array with at least one element',
+          );
+        }
+        this.data = [];
+        for (let i = 0; i < nRows; i++) {
+          if (arrayData[i].length !== nColumns) {
+            throw new RangeError('Inconsistent array dimensions');
+          }
+          if (!isArrayOfNumbers(arrayData[i])) {
+            throw new TypeError('Input data contains non-numeric values');
+          }
+          this.data.push(Float64Array.from(arrayData[i]));
+        }
+      } else {
+        throw new TypeError(
+          'First argument must be a positive number or an array',
+        );
+      }
+      this.rows = nRows;
+      this.columns = nColumns;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.data[rowIndex][columnIndex] = value;
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.data[rowIndex][columnIndex];
+    }
+  
+    removeRow(index) {
+      checkRowIndex(this, index);
+      this.data.splice(index, 1);
+      this.rows -= 1;
+      return this;
+    }
+  
+    addRow(index, array) {
+      if (array === undefined) {
+        array = index;
+        index = this.rows;
+      }
+      checkRowIndex(this, index, true);
+      array = Float64Array.from(checkRowVector(this, array));
+      this.data.splice(index, 0, array);
+      this.rows += 1;
+      return this;
+    }
+  
+    removeColumn(index) {
+      checkColumnIndex(this, index);
+      for (let i = 0; i < this.rows; i++) {
+        const newRow = new Float64Array(this.columns - 1);
+        for (let j = 0; j < index; j++) {
+          newRow[j] = this.data[i][j];
+        }
+        for (let j = index + 1; j < this.columns; j++) {
+          newRow[j - 1] = this.data[i][j];
+        }
+        this.data[i] = newRow;
+      }
+      this.columns -= 1;
+      return this;
+    }
+  
+    addColumn(index, array) {
+      if (typeof array === 'undefined') {
+        array = index;
+        index = this.columns;
+      }
+      checkColumnIndex(this, index, true);
+      array = checkColumnVector(this, array);
+      for (let i = 0; i < this.rows; i++) {
+        const newRow = new Float64Array(this.columns + 1);
+        let j = 0;
+        for (; j < index; j++) {
+          newRow[j] = this.data[i][j];
+        }
+        newRow[j++] = array[i];
+        for (; j < this.columns + 1; j++) {
+          newRow[j] = this.data[i][j - 1];
+        }
+        this.data[i] = newRow;
+      }
+      this.columns += 1;
+      return this;
+    }
+  }
+  
+  installMathOperations(AbstractMatrix, Matrix);
+  
+  class BaseView extends AbstractMatrix {
+    constructor(matrix, rows, columns) {
+      super();
+      this.matrix = matrix;
+      this.rows = rows;
+      this.columns = columns;
+    }
+  }
+  
+  class MatrixColumnView extends BaseView {
+    constructor(matrix, column) {
+      checkColumnIndex(matrix, column);
+      super(matrix, matrix.rows, 1);
+      this.column = column;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(rowIndex, this.column, value);
+      return this;
+    }
+  
+    get(rowIndex) {
+      return this.matrix.get(rowIndex, this.column);
+    }
+  }
+  
+  class MatrixColumnSelectionView extends BaseView {
+    constructor(matrix, columnIndices) {
+      checkColumnIndices(matrix, columnIndices);
+      super(matrix, matrix.rows, columnIndices.length);
+      this.columnIndices = columnIndices;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(rowIndex, this.columnIndices[columnIndex], value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(rowIndex, this.columnIndices[columnIndex]);
+    }
+  }
+  
+  class MatrixFlipColumnView extends BaseView {
+    constructor(matrix) {
+      super(matrix, matrix.rows, matrix.columns);
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(rowIndex, this.columns - columnIndex - 1, value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(rowIndex, this.columns - columnIndex - 1);
+    }
+  }
+  
+  class MatrixFlipRowView extends BaseView {
+    constructor(matrix) {
+      super(matrix, matrix.rows, matrix.columns);
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(this.rows - rowIndex - 1, columnIndex, value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(this.rows - rowIndex - 1, columnIndex);
+    }
+  }
+  
+  class MatrixRowView extends BaseView {
+    constructor(matrix, row) {
+      checkRowIndex(matrix, row);
+      super(matrix, 1, matrix.columns);
+      this.row = row;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(this.row, columnIndex, value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(this.row, columnIndex);
+    }
+  }
+  
+  class MatrixRowSelectionView extends BaseView {
+    constructor(matrix, rowIndices) {
+      checkRowIndices(matrix, rowIndices);
+      super(matrix, rowIndices.length, matrix.columns);
+      this.rowIndices = rowIndices;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(this.rowIndices[rowIndex], columnIndex, value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(this.rowIndices[rowIndex], columnIndex);
+    }
+  }
+  
+  class MatrixSelectionView extends BaseView {
+    constructor(matrix, rowIndices, columnIndices) {
+      checkRowIndices(matrix, rowIndices);
+      checkColumnIndices(matrix, columnIndices);
+      super(matrix, rowIndices.length, columnIndices.length);
+      this.rowIndices = rowIndices;
+      this.columnIndices = columnIndices;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(
+        this.rowIndices[rowIndex],
+        this.columnIndices[columnIndex],
+        value,
+      );
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(
+        this.rowIndices[rowIndex],
+        this.columnIndices[columnIndex],
+      );
+    }
+  }
+  
+  class MatrixSubView extends BaseView {
+    constructor(matrix, startRow, endRow, startColumn, endColumn) {
+      checkRange(matrix, startRow, endRow, startColumn, endColumn);
+      super(matrix, endRow - startRow + 1, endColumn - startColumn + 1);
+      this.startRow = startRow;
+      this.startColumn = startColumn;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(
+        this.startRow + rowIndex,
+        this.startColumn + columnIndex,
+        value,
+      );
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(
+        this.startRow + rowIndex,
+        this.startColumn + columnIndex,
+      );
+    }
+  }
+  
+  class MatrixTransposeView extends BaseView {
+    constructor(matrix) {
+      super(matrix, matrix.columns, matrix.rows);
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.matrix.set(columnIndex, rowIndex, value);
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.matrix.get(columnIndex, rowIndex);
+    }
+  }
+  
+  class WrapperMatrix1D extends AbstractMatrix {
+    constructor(data, options = {}) {
+      const { rows = 1 } = options;
+  
+      if (data.length % rows !== 0) {
+        throw new Error('the data length is not divisible by the number of rows');
+      }
+      super();
+      this.rows = rows;
+      this.columns = data.length / rows;
+      this.data = data;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      let index = this._calculateIndex(rowIndex, columnIndex);
+      this.data[index] = value;
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      let index = this._calculateIndex(rowIndex, columnIndex);
+      return this.data[index];
+    }
+  
+    _calculateIndex(row, column) {
+      return row * this.columns + column;
+    }
+  }
+  
+  class WrapperMatrix2D extends AbstractMatrix {
+    constructor(data) {
+      super();
+      this.data = data;
+      this.rows = data.length;
+      this.columns = data[0].length;
+    }
+  
+    set(rowIndex, columnIndex, value) {
+      this.data[rowIndex][columnIndex] = value;
+      return this;
+    }
+  
+    get(rowIndex, columnIndex) {
+      return this.data[rowIndex][columnIndex];
+    }
+  }
+  
+  function wrap(array, options) {
+    if (isAnyArray.isAnyArray(array)) {
+      if (array[0] && isAnyArray.isAnyArray(array[0])) {
+        return new WrapperMatrix2D(array);
+      } else {
+        return new WrapperMatrix1D(array, options);
+      }
+    } else {
+      throw new Error('the argument is not an array');
+    }
+  }
+  
+  class LuDecomposition {
+    constructor(matrix) {
+      matrix = WrapperMatrix2D.checkMatrix(matrix);
+  
+      let lu = matrix.clone();
+      let rows = lu.rows;
+      let columns = lu.columns;
+      let pivotVector = new Float64Array(rows);
+      let pivotSign = 1;
+      let i, j, k, p, s, t, v;
+      let LUcolj, kmax;
+  
+      for (i = 0; i < rows; i++) {
+        pivotVector[i] = i;
+      }
+  
+      LUcolj = new Float64Array(rows);
+  
+      for (j = 0; j < columns; j++) {
+        for (i = 0; i < rows; i++) {
+          LUcolj[i] = lu.get(i, j);
+        }
+  
+        for (i = 0; i < rows; i++) {
+          kmax = Math.min(i, j);
+          s = 0;
+          for (k = 0; k < kmax; k++) {
+            s += lu.get(i, k) * LUcolj[k];
+          }
+          LUcolj[i] -= s;
+          lu.set(i, j, LUcolj[i]);
+        }
+  
+        p = j;
+        for (i = j + 1; i < rows; i++) {
+          if (Math.abs(LUcolj[i]) > Math.abs(LUcolj[p])) {
+            p = i;
+          }
+        }
+  
+        if (p !== j) {
+          for (k = 0; k < columns; k++) {
+            t = lu.get(p, k);
+            lu.set(p, k, lu.get(j, k));
+            lu.set(j, k, t);
+          }
+  
+          v = pivotVector[p];
+          pivotVector[p] = pivotVector[j];
+          pivotVector[j] = v;
+  
+          pivotSign = -pivotSign;
+        }
+  
+        if (j < rows && lu.get(j, j) !== 0) {
+          for (i = j + 1; i < rows; i++) {
+            lu.set(i, j, lu.get(i, j) / lu.get(j, j));
+          }
+        }
+      }
+  
+      this.LU = lu;
+      this.pivotVector = pivotVector;
+      this.pivotSign = pivotSign;
+    }
+  
+    isSingular() {
+      let data = this.LU;
+      let col = data.columns;
+      for (let j = 0; j < col; j++) {
+        if (data.get(j, j) === 0) {
+          return true;
+        }
+      }
+      return false;
+    }
+  
+    solve(value) {
+      value = Matrix.checkMatrix(value);
+  
+      let lu = this.LU;
+      let rows = lu.rows;
+  
+      if (rows !== value.rows) {
+        throw new Error('Invalid matrix dimensions');
+      }
+      if (this.isSingular()) {
+        throw new Error('LU matrix is singular');
+      }
+  
+      let count = value.columns;
+      let X = value.subMatrixRow(this.pivotVector, 0, count - 1);
+      let columns = lu.columns;
+      let i, j, k;
+  
+      for (k = 0; k < columns; k++) {
+        for (i = k + 1; i < columns; i++) {
+          for (j = 0; j < count; j++) {
+            X.set(i, j, X.get(i, j) - X.get(k, j) * lu.get(i, k));
+          }
+        }
+      }
+      for (k = columns - 1; k >= 0; k--) {
+        for (j = 0; j < count; j++) {
+          X.set(k, j, X.get(k, j) / lu.get(k, k));
+        }
+        for (i = 0; i < k; i++) {
+          for (j = 0; j < count; j++) {
+            X.set(i, j, X.get(i, j) - X.get(k, j) * lu.get(i, k));
+          }
+        }
+      }
+      return X;
+    }
+  
+    get determinant() {
+      let data = this.LU;
+      if (!data.isSquare()) {
+        throw new Error('Matrix must be square');
+      }
+      let determinant = this.pivotSign;
+      let col = data.columns;
+      for (let j = 0; j < col; j++) {
+        determinant *= data.get(j, j);
+      }
+      return determinant;
+    }
+  
+    get lowerTriangularMatrix() {
+      let data = this.LU;
+      let rows = data.rows;
+      let columns = data.columns;
+      let X = new Matrix(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          if (i > j) {
+            X.set(i, j, data.get(i, j));
+          } else if (i === j) {
+            X.set(i, j, 1);
+          } else {
+            X.set(i, j, 0);
+          }
+        }
+      }
+      return X;
+    }
+  
+    get upperTriangularMatrix() {
+      let data = this.LU;
+      let rows = data.rows;
+      let columns = data.columns;
+      let X = new Matrix(rows, columns);
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < columns; j++) {
+          if (i <= j) {
+            X.set(i, j, data.get(i, j));
+          } else {
+            X.set(i, j, 0);
+          }
+        }
+      }
+      return X;
+    }
+  
+    get pivotPermutationVector() {
+      return Array.from(this.pivotVector);
+    }
+  }
+  
+  function hypotenuse(a, b) {
+    let r = 0;
+    if (Math.abs(a) > Math.abs(b)) {
+      r = b / a;
+      return Math.abs(a) * Math.sqrt(1 + r * r);
+    }
+    if (b !== 0) {
+      r = a / b;
+      return Math.abs(b) * Math.sqrt(1 + r * r);
+    }
+    return 0;
+  }
+  
+  class QrDecomposition {
+    constructor(value) {
+      value = WrapperMatrix2D.checkMatrix(value);
+  
+      let qr = value.clone();
+      let m = value.rows;
+      let n = value.columns;
+      let rdiag = new Float64Array(n);
+      let i, j, k, s;
+  
+      for (k = 0; k < n; k++) {
+        let nrm = 0;
+        for (i = k; i < m; i++) {
+          nrm = hypotenuse(nrm, qr.get(i, k));
+        }
+        if (nrm !== 0) {
+          if (qr.get(k, k) < 0) {
+            nrm = -nrm;
+          }
+          for (i = k; i < m; i++) {
+            qr.set(i, k, qr.get(i, k) / nrm);
+          }
+          qr.set(k, k, qr.get(k, k) + 1);
+          for (j = k + 1; j < n; j++) {
+            s = 0;
+            for (i = k; i < m; i++) {
+              s += qr.get(i, k) * qr.get(i, j);
+            }
+            s = -s / qr.get(k, k);
+            for (i = k; i < m; i++) {
+              qr.set(i, j, qr.get(i, j) + s * qr.get(i, k));
+            }
+          }
+        }
+        rdiag[k] = -nrm;
+      }
+  
+      this.QR = qr;
+      this.Rdiag = rdiag;
+    }
+  
+    solve(value) {
+      value = Matrix.checkMatrix(value);
+  
+      let qr = this.QR;
+      let m = qr.rows;
+  
+      if (value.rows !== m) {
+        throw new Error('Matrix row dimensions must agree');
+      }
+      if (!this.isFullRank()) {
+        throw new Error('Matrix is rank deficient');
+      }
+  
+      let count = value.columns;
+      let X = value.clone();
+      let n = qr.columns;
+      let i, j, k, s;
+  
+      for (k = 0; k < n; k++) {
+        for (j = 0; j < count; j++) {
+          s = 0;
+          for (i = k; i < m; i++) {
+            s += qr.get(i, k) * X.get(i, j);
+          }
+          s = -s / qr.get(k, k);
+          for (i = k; i < m; i++) {
+            X.set(i, j, X.get(i, j) + s * qr.get(i, k));
+          }
+        }
+      }
+      for (k = n - 1; k >= 0; k--) {
+        for (j = 0; j < count; j++) {
+          X.set(k, j, X.get(k, j) / this.Rdiag[k]);
+        }
+        for (i = 0; i < k; i++) {
+          for (j = 0; j < count; j++) {
+            X.set(i, j, X.get(i, j) - X.get(k, j) * qr.get(i, k));
+          }
+        }
+      }
+  
+      return X.subMatrix(0, n - 1, 0, count - 1);
+    }
+  
+    isFullRank() {
+      let columns = this.QR.columns;
+      for (let i = 0; i < columns; i++) {
+        if (this.Rdiag[i] === 0) {
+          return false;
+        }
+      }
+      return true;
+    }
+  
+    get upperTriangularMatrix() {
+      let qr = this.QR;
+      let n = qr.columns;
+      let X = new Matrix(n, n);
+      let i, j;
+      for (i = 0; i < n; i++) {
+        for (j = 0; j < n; j++) {
+          if (i < j) {
+            X.set(i, j, qr.get(i, j));
+          } else if (i === j) {
+            X.set(i, j, this.Rdiag[i]);
+          } else {
+            X.set(i, j, 0);
+          }
+        }
+      }
+      return X;
+    }
+  
+    get orthogonalMatrix() {
+      let qr = this.QR;
+      let rows = qr.rows;
+      let columns = qr.columns;
+      let X = new Matrix(rows, columns);
+      let i, j, k, s;
+  
+      for (k = columns - 1; k >= 0; k--) {
+        for (i = 0; i < rows; i++) {
+          X.set(i, k, 0);
+        }
+        X.set(k, k, 1);
+        for (j = k; j < columns; j++) {
+          if (qr.get(k, k) !== 0) {
+            s = 0;
+            for (i = k; i < rows; i++) {
+              s += qr.get(i, k) * X.get(i, j);
+            }
+  
+            s = -s / qr.get(k, k);
+  
+            for (i = k; i < rows; i++) {
+              X.set(i, j, X.get(i, j) + s * qr.get(i, k));
+            }
+          }
+        }
+      }
+      return X;
+    }
+  }
+  
+  class SingularValueDecomposition {
+    constructor(value, options = {}) {
+      value = WrapperMatrix2D.checkMatrix(value);
+  
+      if (value.isEmpty()) {
+        throw new Error('Matrix must be non-empty');
+      }
+  
+      let m = value.rows;
+      let n = value.columns;
+  
+      const {
+        computeLeftSingularVectors = true,
+        computeRightSingularVectors = true,
+        autoTranspose = false,
+      } = options;
+  
+      let wantu = Boolean(computeLeftSingularVectors);
+      let wantv = Boolean(computeRightSingularVectors);
+  
+      let swapped = false;
+      let a;
+      if (m < n) {
+        if (!autoTranspose) {
+          a = value.clone();
+          // eslint-disable-next-line no-console
+          console.warn(
+            'Computing SVD on a matrix with more columns than rows. Consider enabling autoTranspose',
+          );
+        } else {
+          a = value.transpose();
+          m = a.rows;
+          n = a.columns;
+          swapped = true;
+          let aux = wantu;
+          wantu = wantv;
+          wantv = aux;
+        }
+      } else {
+        a = value.clone();
+      }
+  
+      let nu = Math.min(m, n);
+      let ni = Math.min(m + 1, n);
+      let s = new Float64Array(ni);
+      let U = new Matrix(m, nu);
+      let V = new Matrix(n, n);
+  
+      let e = new Float64Array(n);
+      let work = new Float64Array(m);
+  
+      let si = new Float64Array(ni);
+      for (let i = 0; i < ni; i++) si[i] = i;
+  
+      let nct = Math.min(m - 1, n);
+      let nrt = Math.max(0, Math.min(n - 2, m));
+      let mrc = Math.max(nct, nrt);
+  
+      for (let k = 0; k < mrc; k++) {
+        if (k < nct) {
+          s[k] = 0;
+          for (let i = k; i < m; i++) {
+            s[k] = hypotenuse(s[k], a.get(i, k));
+          }
+          if (s[k] !== 0) {
+            if (a.get(k, k) < 0) {
+              s[k] = -s[k];
+            }
+            for (let i = k; i < m; i++) {
+              a.set(i, k, a.get(i, k) / s[k]);
+            }
+            a.set(k, k, a.get(k, k) + 1);
+          }
+          s[k] = -s[k];
+        }
+  
+        for (let j = k + 1; j < n; j++) {
+          if (k < nct && s[k] !== 0) {
+            let t = 0;
+            for (let i = k; i < m; i++) {
+              t += a.get(i, k) * a.get(i, j);
+            }
+            t = -t / a.get(k, k);
+            for (let i = k; i < m; i++) {
+              a.set(i, j, a.get(i, j) + t * a.get(i, k));
+            }
+          }
+          e[j] = a.get(k, j);
+        }
+  
+        if (wantu && k < nct) {
+          for (let i = k; i < m; i++) {
+            U.set(i, k, a.get(i, k));
+          }
+        }
+  
+        if (k < nrt) {
+          e[k] = 0;
+          for (let i = k + 1; i < n; i++) {
+            e[k] = hypotenuse(e[k], e[i]);
+          }
+          if (e[k] !== 0) {
+            if (e[k + 1] < 0) {
+              e[k] = 0 - e[k];
+            }
+            for (let i = k + 1; i < n; i++) {
+              e[i] /= e[k];
+            }
+            e[k + 1] += 1;
+          }
+          e[k] = -e[k];
+          if (k + 1 < m && e[k] !== 0) {
+            for (let i = k + 1; i < m; i++) {
+              work[i] = 0;
+            }
+            for (let i = k + 1; i < m; i++) {
+              for (let j = k + 1; j < n; j++) {
+                work[i] += e[j] * a.get(i, j);
+              }
+            }
+            for (let j = k + 1; j < n; j++) {
+              let t = -e[j] / e[k + 1];
+              for (let i = k + 1; i < m; i++) {
+                a.set(i, j, a.get(i, j) + t * work[i]);
+              }
+            }
+          }
+          if (wantv) {
+            for (let i = k + 1; i < n; i++) {
+              V.set(i, k, e[i]);
+            }
+          }
+        }
+      }
+  
+      let p = Math.min(n, m + 1);
+      if (nct < n) {
+        s[nct] = a.get(nct, nct);
+      }
+      if (m < p) {
+        s[p - 1] = 0;
+      }
+      if (nrt + 1 < p) {
+        e[nrt] = a.get(nrt, p - 1);
+      }
+      e[p - 1] = 0;
+  
+      if (wantu) {
+        for (let j = nct; j < nu; j++) {
+          for (let i = 0; i < m; i++) {
+            U.set(i, j, 0);
+          }
+          U.set(j, j, 1);
+        }
+        for (let k = nct - 1; k >= 0; k--) {
+          if (s[k] !== 0) {
+            for (let j = k + 1; j < nu; j++) {
+              let t = 0;
+              for (let i = k; i < m; i++) {
+                t += U.get(i, k) * U.get(i, j);
+              }
+              t = -t / U.get(k, k);
+              for (let i = k; i < m; i++) {
+                U.set(i, j, U.get(i, j) + t * U.get(i, k));
+              }
+            }
+            for (let i = k; i < m; i++) {
+              U.set(i, k, -U.get(i, k));
+            }
+            U.set(k, k, 1 + U.get(k, k));
+            for (let i = 0; i < k - 1; i++) {
+              U.set(i, k, 0);
+            }
+          } else {
+            for (let i = 0; i < m; i++) {
+              U.set(i, k, 0);
+            }
+            U.set(k, k, 1);
+          }
+        }
+      }
+  
+      if (wantv) {
+        for (let k = n - 1; k >= 0; k--) {
+          if (k < nrt && e[k] !== 0) {
+            for (let j = k + 1; j < n; j++) {
+              let t = 0;
+              for (let i = k + 1; i < n; i++) {
+                t += V.get(i, k) * V.get(i, j);
+              }
+              t = -t / V.get(k + 1, k);
+              for (let i = k + 1; i < n; i++) {
+                V.set(i, j, V.get(i, j) + t * V.get(i, k));
+              }
+            }
+          }
+          for (let i = 0; i < n; i++) {
+            V.set(i, k, 0);
+          }
+          V.set(k, k, 1);
+        }
+      }
+  
+      let pp = p - 1;
+      let eps = Number.EPSILON;
+      while (p > 0) {
+        let k, kase;
+        for (k = p - 2; k >= -1; k--) {
+          if (k === -1) {
+            break;
+          }
+          const alpha =
+            Number.MIN_VALUE + eps * Math.abs(s[k] + Math.abs(s[k + 1]));
+          if (Math.abs(e[k]) <= alpha || Number.isNaN(e[k])) {
+            e[k] = 0;
+            break;
+          }
+        }
+        if (k === p - 2) {
+          kase = 4;
+        } else {
+          let ks;
+          for (ks = p - 1; ks >= k; ks--) {
+            if (ks === k) {
+              break;
+            }
+            let t =
+              (ks !== p ? Math.abs(e[ks]) : 0) +
+              (ks !== k + 1 ? Math.abs(e[ks - 1]) : 0);
+            if (Math.abs(s[ks]) <= eps * t) {
+              s[ks] = 0;
+              break;
+            }
+          }
+          if (ks === k) {
+            kase = 3;
+          } else if (ks === p - 1) {
+            kase = 1;
+          } else {
+            kase = 2;
+            k = ks;
+          }
+        }
+  
+        k++;
+  
+        switch (kase) {
+          case 1: {
+            let f = e[p - 2];
+            e[p - 2] = 0;
+            for (let j = p - 2; j >= k; j--) {
+              let t = hypotenuse(s[j], f);
+              let cs = s[j] / t;
+              let sn = f / t;
+              s[j] = t;
+              if (j !== k) {
+                f = -sn * e[j - 1];
+                e[j - 1] = cs * e[j - 1];
+              }
+              if (wantv) {
+                for (let i = 0; i < n; i++) {
+                  t = cs * V.get(i, j) + sn * V.get(i, p - 1);
+                  V.set(i, p - 1, -sn * V.get(i, j) + cs * V.get(i, p - 1));
+                  V.set(i, j, t);
+                }
+              }
+            }
+            break;
+          }
+          case 2: {
+            let f = e[k - 1];
+            e[k - 1] = 0;
+            for (let j = k; j < p; j++) {
+              let t = hypotenuse(s[j], f);
+              let cs = s[j] / t;
+              let sn = f / t;
+              s[j] = t;
+              f = -sn * e[j];
+              e[j] = cs * e[j];
+              if (wantu) {
+                for (let i = 0; i < m; i++) {
+                  t = cs * U.get(i, j) + sn * U.get(i, k - 1);
+                  U.set(i, k - 1, -sn * U.get(i, j) + cs * U.get(i, k - 1));
+                  U.set(i, j, t);
+                }
+              }
+            }
+            break;
+          }
+          case 3: {
+            const scale = Math.max(
+              Math.abs(s[p - 1]),
+              Math.abs(s[p - 2]),
+              Math.abs(e[p - 2]),
+              Math.abs(s[k]),
+              Math.abs(e[k]),
+            );
+            const sp = s[p - 1] / scale;
+            const spm1 = s[p - 2] / scale;
+            const epm1 = e[p - 2] / scale;
+            const sk = s[k] / scale;
+            const ek = e[k] / scale;
+            const b = ((spm1 + sp) * (spm1 - sp) + epm1 * epm1) / 2;
+            const c = sp * epm1 * (sp * epm1);
+            let shift = 0;
+            if (b !== 0 || c !== 0) {
+              if (b < 0) {
+                shift = 0 - Math.sqrt(b * b + c);
+              } else {
+                shift = Math.sqrt(b * b + c);
+              }
+              shift = c / (b + shift);
+            }
+            let f = (sk + sp) * (sk - sp) + shift;
+            let g = sk * ek;
+            for (let j = k; j < p - 1; j++) {
+              let t = hypotenuse(f, g);
+              if (t === 0) t = Number.MIN_VALUE;
+              let cs = f / t;
+              let sn = g / t;
+              if (j !== k) {
+                e[j - 1] = t;
+              }
+              f = cs * s[j] + sn * e[j];
+              e[j] = cs * e[j] - sn * s[j];
+              g = sn * s[j + 1];
+              s[j + 1] = cs * s[j + 1];
+              if (wantv) {
+                for (let i = 0; i < n; i++) {
+                  t = cs * V.get(i, j) + sn * V.get(i, j + 1);
+                  V.set(i, j + 1, -sn * V.get(i, j) + cs * V.get(i, j + 1));
+                  V.set(i, j, t);
+                }
+              }
+              t = hypotenuse(f, g);
+              if (t === 0) t = Number.MIN_VALUE;
+              cs = f / t;
+              sn = g / t;
+              s[j] = t;
+              f = cs * e[j] + sn * s[j + 1];
+              s[j + 1] = -sn * e[j] + cs * s[j + 1];
+              g = sn * e[j + 1];
+              e[j + 1] = cs * e[j + 1];
+              if (wantu && j < m - 1) {
+                for (let i = 0; i < m; i++) {
+                  t = cs * U.get(i, j) + sn * U.get(i, j + 1);
+                  U.set(i, j + 1, -sn * U.get(i, j) + cs * U.get(i, j + 1));
+                  U.set(i, j, t);
+                }
+              }
+            }
+            e[p - 2] = f;
+            break;
+          }
+          case 4: {
+            if (s[k] <= 0) {
+              s[k] = s[k] < 0 ? -s[k] : 0;
+              if (wantv) {
+                for (let i = 0; i <= pp; i++) {
+                  V.set(i, k, -V.get(i, k));
+                }
+              }
+            }
+            while (k < pp) {
+              if (s[k] >= s[k + 1]) {
+                break;
+              }
+              let t = s[k];
+              s[k] = s[k + 1];
+              s[k + 1] = t;
+              if (wantv && k < n - 1) {
+                for (let i = 0; i < n; i++) {
+                  t = V.get(i, k + 1);
+                  V.set(i, k + 1, V.get(i, k));
+                  V.set(i, k, t);
+                }
+              }
+              if (wantu && k < m - 1) {
+                for (let i = 0; i < m; i++) {
+                  t = U.get(i, k + 1);
+                  U.set(i, k + 1, U.get(i, k));
+                  U.set(i, k, t);
+                }
+              }
+              k++;
+            }
+            p--;
+            break;
+          }
+          // no default
+        }
+      }
+  
+      if (swapped) {
+        let tmp = V;
+        V = U;
+        U = tmp;
+      }
+  
+      this.m = m;
+      this.n = n;
+      this.s = s;
+      this.U = U;
+      this.V = V;
+    }
+  
+    solve(value) {
+      let Y = value;
+      let e = this.threshold;
+      let scols = this.s.length;
+      let Ls = Matrix.zeros(scols, scols);
+  
+      for (let i = 0; i < scols; i++) {
+        if (Math.abs(this.s[i]) <= e) {
+          Ls.set(i, i, 0);
+        } else {
+          Ls.set(i, i, 1 / this.s[i]);
+        }
+      }
+  
+      let U = this.U;
+      let V = this.rightSingularVectors;
+  
+      let VL = V.mmul(Ls);
+      let vrows = V.rows;
+      let urows = U.rows;
+      let VLU = Matrix.zeros(vrows, urows);
+  
+      for (let i = 0; i < vrows; i++) {
+        for (let j = 0; j < urows; j++) {
+          let sum = 0;
+          for (let k = 0; k < scols; k++) {
+            sum += VL.get(i, k) * U.get(j, k);
+          }
+          VLU.set(i, j, sum);
+        }
+      }
+  
+      return VLU.mmul(Y);
+    }
+  
+    solveForDiagonal(value) {
+      return this.solve(Matrix.diag(value));
+    }
+  
+    inverse() {
+      let V = this.V;
+      let e = this.threshold;
+      let vrows = V.rows;
+      let vcols = V.columns;
+      let X = new Matrix(vrows, this.s.length);
+  
+      for (let i = 0; i < vrows; i++) {
+        for (let j = 0; j < vcols; j++) {
+          if (Math.abs(this.s[j]) > e) {
+            X.set(i, j, V.get(i, j) / this.s[j]);
+          }
+        }
+      }
+  
+      let U = this.U;
+  
+      let urows = U.rows;
+      let ucols = U.columns;
+      let Y = new Matrix(vrows, urows);
+  
+      for (let i = 0; i < vrows; i++) {
+        for (let j = 0; j < urows; j++) {
+          let sum = 0;
+          for (let k = 0; k < ucols; k++) {
+            sum += X.get(i, k) * U.get(j, k);
+          }
+          Y.set(i, j, sum);
+        }
+      }
+  
+      return Y;
+    }
+  
+    get condition() {
+      return this.s[0] / this.s[Math.min(this.m, this.n) - 1];
+    }
+  
+    get norm2() {
+      return this.s[0];
+    }
+  
+    get rank() {
+      let tol = Math.max(this.m, this.n) * this.s[0] * Number.EPSILON;
+      let r = 0;
+      let s = this.s;
+      for (let i = 0, ii = s.length; i < ii; i++) {
+        if (s[i] > tol) {
+          r++;
+        }
+      }
+      return r;
+    }
+  
+    get diagonal() {
+      return Array.from(this.s);
+    }
+  
+    get threshold() {
+      return (Number.EPSILON / 2) * Math.max(this.m, this.n) * this.s[0];
+    }
+  
+    get leftSingularVectors() {
+      return this.U;
+    }
+  
+    get rightSingularVectors() {
+      return this.V;
+    }
+  
+    get diagonalMatrix() {
+      return Matrix.diag(this.s);
+    }
+  }
+  
+  function inverse(matrix, useSVD = false) {
+    matrix = WrapperMatrix2D.checkMatrix(matrix);
+    if (useSVD) {
+      return new SingularValueDecomposition(matrix).inverse();
+    } else {
+      return solve(matrix, Matrix.eye(matrix.rows));
+    }
+  }
+  
+  function solve(leftHandSide, rightHandSide, useSVD = false) {
+    leftHandSide = WrapperMatrix2D.checkMatrix(leftHandSide);
+    rightHandSide = WrapperMatrix2D.checkMatrix(rightHandSide);
+    if (useSVD) {
+      return new SingularValueDecomposition(leftHandSide).solve(rightHandSide);
+    } else {
+      return leftHandSide.isSquare()
+        ? new LuDecomposition(leftHandSide).solve(rightHandSide)
+        : new QrDecomposition(leftHandSide).solve(rightHandSide);
+    }
+  }
+  
+  function determinant(matrix) {
+    matrix = Matrix.checkMatrix(matrix);
+    if (matrix.isSquare()) {
+      if (matrix.columns === 0) {
+        return 1;
+      }
+  
+      let a, b, c, d;
+      if (matrix.columns === 2) {
+        // 2 x 2 matrix
+        a = matrix.get(0, 0);
+        b = matrix.get(0, 1);
+        c = matrix.get(1, 0);
+        d = matrix.get(1, 1);
+  
+        return a * d - b * c;
+      } else if (matrix.columns === 3) {
+        // 3 x 3 matrix
+        let subMatrix0, subMatrix1, subMatrix2;
+        subMatrix0 = new MatrixSelectionView(matrix, [1, 2], [1, 2]);
+        subMatrix1 = new MatrixSelectionView(matrix, [1, 2], [0, 2]);
+        subMatrix2 = new MatrixSelectionView(matrix, [1, 2], [0, 1]);
+        a = matrix.get(0, 0);
+        b = matrix.get(0, 1);
+        c = matrix.get(0, 2);
+  
+        return (
+          a * determinant(subMatrix0) -
+          b * determinant(subMatrix1) +
+          c * determinant(subMatrix2)
+        );
+      } else {
+        // general purpose determinant using the LU decomposition
+        return new LuDecomposition(matrix).determinant;
+      }
+    } else {
+      throw Error('determinant can only be calculated for a square matrix');
+    }
+  }
+  
+  function xrange(n, exception) {
+    let range = [];
+    for (let i = 0; i < n; i++) {
+      if (i !== exception) {
+        range.push(i);
+      }
+    }
+    return range;
+  }
+  
+  function dependenciesOneRow(
+    error,
+    matrix,
+    index,
+    thresholdValue = 10e-10,
+    thresholdError = 10e-10,
+  ) {
+    if (error > thresholdError) {
+      return new Array(matrix.rows + 1).fill(0);
+    } else {
+      let returnArray = matrix.addRow(index, [0]);
+      for (let i = 0; i < returnArray.rows; i++) {
+        if (Math.abs(returnArray.get(i, 0)) < thresholdValue) {
+          returnArray.set(i, 0, 0);
+        }
+      }
+      return returnArray.to1DArray();
+    }
+  }
+  
+  function linearDependencies(matrix, options = {}) {
+    const { thresholdValue = 10e-10, thresholdError = 10e-10 } = options;
+    matrix = Matrix.checkMatrix(matrix);
+  
+    let n = matrix.rows;
+    let results = new Matrix(n, n);
+  
+    for (let i = 0; i < n; i++) {
+      let b = Matrix.columnVector(matrix.getRow(i));
+      let Abis = matrix.subMatrixRow(xrange(n, i)).transpose();
+      let svd = new SingularValueDecomposition(Abis);
+      let x = svd.solve(b);
+      let error = Matrix.sub(b, Abis.mmul(x)).abs().max();
+      results.setRow(
+        i,
+        dependenciesOneRow(error, x, i, thresholdValue, thresholdError),
+      );
+    }
+    return results;
+  }
+  
+  function pseudoInverse(matrix, threshold = Number.EPSILON) {
+    matrix = Matrix.checkMatrix(matrix);
+    if (matrix.isEmpty()) {
+      // with a zero dimension, the pseudo-inverse is the transpose, since all 0xn and nx0 matrices are singular
+      // (0xn)*(nx0)*(0xn) = 0xn
+      // (nx0)*(0xn)*(nx0) = nx0
+      return matrix.transpose();
+    }
+    let svdSolution = new SingularValueDecomposition(matrix, { autoTranspose: true });
+  
+    let U = svdSolution.leftSingularVectors;
+    let V = svdSolution.rightSingularVectors;
+    let s = svdSolution.diagonal;
+  
+    for (let i = 0; i < s.length; i++) {
+      if (Math.abs(s[i]) > threshold) {
+        s[i] = 1.0 / s[i];
+      } else {
+        s[i] = 0.0;
+      }
+    }
+  
+    return V.mmul(Matrix.diag(s).mmul(U.transpose()));
+  }
+  
+  function covariance(xMatrix, yMatrix = xMatrix, options = {}) {
+    xMatrix = new Matrix(xMatrix);
+    let yIsSame = false;
+    if (
+      typeof yMatrix === 'object' &&
+      !Matrix.isMatrix(yMatrix) &&
+      !isAnyArray.isAnyArray(yMatrix)
+    ) {
+      options = yMatrix;
+      yMatrix = xMatrix;
+      yIsSame = true;
+    } else {
+      yMatrix = new Matrix(yMatrix);
+    }
+    if (xMatrix.rows !== yMatrix.rows) {
+      throw new TypeError('Both matrices must have the same number of rows');
+    }
+    const { center = true } = options;
+    if (center) {
+      xMatrix = xMatrix.center('column');
+      if (!yIsSame) {
+        yMatrix = yMatrix.center('column');
+      }
+    }
+    const cov = xMatrix.transpose().mmul(yMatrix);
+    for (let i = 0; i < cov.rows; i++) {
+      for (let j = 0; j < cov.columns; j++) {
+        cov.set(i, j, cov.get(i, j) * (1 / (xMatrix.rows - 1)));
+      }
+    }
+    return cov;
+  }
+  
+  function correlation(xMatrix, yMatrix = xMatrix, options = {}) {
+    xMatrix = new Matrix(xMatrix);
+    let yIsSame = false;
+    if (
+      typeof yMatrix === 'object' &&
+      !Matrix.isMatrix(yMatrix) &&
+      !isAnyArray.isAnyArray(yMatrix)
+    ) {
+      options = yMatrix;
+      yMatrix = xMatrix;
+      yIsSame = true;
+    } else {
+      yMatrix = new Matrix(yMatrix);
+    }
+    if (xMatrix.rows !== yMatrix.rows) {
+      throw new TypeError('Both matrices must have the same number of rows');
+    }
+  
+    const { center = true, scale = true } = options;
+    if (center) {
+      xMatrix.center('column');
+      if (!yIsSame) {
+        yMatrix.center('column');
+      }
+    }
+    if (scale) {
+      xMatrix.scale('column');
+      if (!yIsSame) {
+        yMatrix.scale('column');
+      }
+    }
+  
+    const sdx = xMatrix.standardDeviation('column', { unbiased: true });
+    const sdy = yIsSame
+      ? sdx
+      : yMatrix.standardDeviation('column', { unbiased: true });
+  
+    const corr = xMatrix.transpose().mmul(yMatrix);
+    for (let i = 0; i < corr.rows; i++) {
+      for (let j = 0; j < corr.columns; j++) {
+        corr.set(
+          i,
+          j,
+          corr.get(i, j) * (1 / (sdx[i] * sdy[j])) * (1 / (xMatrix.rows - 1)),
+        );
+      }
+    }
+    return corr;
+  }
+  
+  class EigenvalueDecomposition {
+    constructor(matrix, options = {}) {
+      const { assumeSymmetric = false } = options;
+  
+      matrix = WrapperMatrix2D.checkMatrix(matrix);
+      if (!matrix.isSquare()) {
+        throw new Error('Matrix is not a square matrix');
+      }
+  
+      if (matrix.isEmpty()) {
+        throw new Error('Matrix must be non-empty');
+      }
+  
+      let n = matrix.columns;
+      let V = new Matrix(n, n);
+      let d = new Float64Array(n);
+      let e = new Float64Array(n);
+      let value = matrix;
+      let i, j;
+  
+      let isSymmetric = false;
+      if (assumeSymmetric) {
+        isSymmetric = true;
+      } else {
+        isSymmetric = matrix.isSymmetric();
+      }
+  
+      if (isSymmetric) {
+        for (i = 0; i < n; i++) {
+          for (j = 0; j < n; j++) {
+            V.set(i, j, value.get(i, j));
+          }
+        }
+        tred2(n, e, d, V);
+        tql2(n, e, d, V);
+      } else {
+        let H = new Matrix(n, n);
+        let ort = new Float64Array(n);
+        for (j = 0; j < n; j++) {
+          for (i = 0; i < n; i++) {
+            H.set(i, j, value.get(i, j));
+          }
+        }
+        orthes(n, H, ort, V);
+        hqr2(n, e, d, V, H);
+      }
+  
+      this.n = n;
+      this.e = e;
+      this.d = d;
+      this.V = V;
+    }
+  
+    get realEigenvalues() {
+      return Array.from(this.d);
+    }
+  
+    get imaginaryEigenvalues() {
+      return Array.from(this.e);
+    }
+  
+    get eigenvectorMatrix() {
+      return this.V;
+    }
+  
+    get diagonalMatrix() {
+      let n = this.n;
+      let e = this.e;
+      let d = this.d;
+      let X = new Matrix(n, n);
+      let i, j;
+      for (i = 0; i < n; i++) {
+        for (j = 0; j < n; j++) {
+          X.set(i, j, 0);
+        }
+        X.set(i, i, d[i]);
+        if (e[i] > 0) {
+          X.set(i, i + 1, e[i]);
+        } else if (e[i] < 0) {
+          X.set(i, i - 1, e[i]);
+        }
+      }
+      return X;
+    }
+  }
+  
+  function tred2(n, e, d, V) {
+    let f, g, h, i, j, k, hh, scale;
+  
+    for (j = 0; j < n; j++) {
+      d[j] = V.get(n - 1, j);
+    }
+  
+    for (i = n - 1; i > 0; i--) {
+      scale = 0;
+      h = 0;
+      for (k = 0; k < i; k++) {
+        scale = scale + Math.abs(d[k]);
+      }
+  
+      if (scale === 0) {
+        e[i] = d[i - 1];
+        for (j = 0; j < i; j++) {
+          d[j] = V.get(i - 1, j);
+          V.set(i, j, 0);
+          V.set(j, i, 0);
+        }
+      } else {
+        for (k = 0; k < i; k++) {
+          d[k] /= scale;
+          h += d[k] * d[k];
+        }
+  
+        f = d[i - 1];
+        g = Math.sqrt(h);
+        if (f > 0) {
+          g = -g;
+        }
+  
+        e[i] = scale * g;
+        h = h - f * g;
+        d[i - 1] = f - g;
+        for (j = 0; j < i; j++) {
+          e[j] = 0;
+        }
+  
+        for (j = 0; j < i; j++) {
+          f = d[j];
+          V.set(j, i, f);
+          g = e[j] + V.get(j, j) * f;
+          for (k = j + 1; k <= i - 1; k++) {
+            g += V.get(k, j) * d[k];
+            e[k] += V.get(k, j) * f;
+          }
+          e[j] = g;
+        }
+  
+        f = 0;
+        for (j = 0; j < i; j++) {
+          e[j] /= h;
+          f += e[j] * d[j];
+        }
+  
+        hh = f / (h + h);
+        for (j = 0; j < i; j++) {
+          e[j] -= hh * d[j];
+        }
+  
+        for (j = 0; j < i; j++) {
+          f = d[j];
+          g = e[j];
+          for (k = j; k <= i - 1; k++) {
+            V.set(k, j, V.get(k, j) - (f * e[k] + g * d[k]));
+          }
+          d[j] = V.get(i - 1, j);
+          V.set(i, j, 0);
+        }
+      }
+      d[i] = h;
+    }
+  
+    for (i = 0; i < n - 1; i++) {
+      V.set(n - 1, i, V.get(i, i));
+      V.set(i, i, 1);
+      h = d[i + 1];
+      if (h !== 0) {
+        for (k = 0; k <= i; k++) {
+          d[k] = V.get(k, i + 1) / h;
+        }
+  
+        for (j = 0; j <= i; j++) {
+          g = 0;
+          for (k = 0; k <= i; k++) {
+            g += V.get(k, i + 1) * V.get(k, j);
+          }
+          for (k = 0; k <= i; k++) {
+            V.set(k, j, V.get(k, j) - g * d[k]);
+          }
+        }
+      }
+  
+      for (k = 0; k <= i; k++) {
+        V.set(k, i + 1, 0);
+      }
+    }
+  
+    for (j = 0; j < n; j++) {
+      d[j] = V.get(n - 1, j);
+      V.set(n - 1, j, 0);
+    }
+  
+    V.set(n - 1, n - 1, 1);
+    e[0] = 0;
+  }
+  
+  function tql2(n, e, d, V) {
+    let g, h, i, j, k, l, m, p, r, dl1, c, c2, c3, el1, s, s2;
+  
+    for (i = 1; i < n; i++) {
+      e[i - 1] = e[i];
+    }
+  
+    e[n - 1] = 0;
+  
+    let f = 0;
+    let tst1 = 0;
+    let eps = Number.EPSILON;
+  
+    for (l = 0; l < n; l++) {
+      tst1 = Math.max(tst1, Math.abs(d[l]) + Math.abs(e[l]));
+      m = l;
+      while (m < n) {
+        if (Math.abs(e[m]) <= eps * tst1) {
+          break;
+        }
+        m++;
+      }
+  
+      if (m > l) {
+        do {
+  
+          g = d[l];
+          p = (d[l + 1] - g) / (2 * e[l]);
+          r = hypotenuse(p, 1);
+          if (p < 0) {
+            r = -r;
+          }
+  
+          d[l] = e[l] / (p + r);
+          d[l + 1] = e[l] * (p + r);
+          dl1 = d[l + 1];
+          h = g - d[l];
+          for (i = l + 2; i < n; i++) {
+            d[i] -= h;
+          }
+  
+          f = f + h;
+  
+          p = d[m];
+          c = 1;
+          c2 = c;
+          c3 = c;
+          el1 = e[l + 1];
+          s = 0;
+          s2 = 0;
+          for (i = m - 1; i >= l; i--) {
+            c3 = c2;
+            c2 = c;
+            s2 = s;
+            g = c * e[i];
+            h = c * p;
+            r = hypotenuse(p, e[i]);
+            e[i + 1] = s * r;
+            s = e[i] / r;
+            c = p / r;
+            p = c * d[i] - s * g;
+            d[i + 1] = h + s * (c * g + s * d[i]);
+  
+            for (k = 0; k < n; k++) {
+              h = V.get(k, i + 1);
+              V.set(k, i + 1, s * V.get(k, i) + c * h);
+              V.set(k, i, c * V.get(k, i) - s * h);
+            }
+          }
+  
+          p = (-s * s2 * c3 * el1 * e[l]) / dl1;
+          e[l] = s * p;
+          d[l] = c * p;
+        } while (Math.abs(e[l]) > eps * tst1);
+      }
+      d[l] = d[l] + f;
+      e[l] = 0;
+    }
+  
+    for (i = 0; i < n - 1; i++) {
+      k = i;
+      p = d[i];
+      for (j = i + 1; j < n; j++) {
+        if (d[j] < p) {
+          k = j;
+          p = d[j];
+        }
+      }
+  
+      if (k !== i) {
+        d[k] = d[i];
+        d[i] = p;
+        for (j = 0; j < n; j++) {
+          p = V.get(j, i);
+          V.set(j, i, V.get(j, k));
+          V.set(j, k, p);
+        }
+      }
+    }
+  }
+  
+  function orthes(n, H, ort, V) {
+    let low = 0;
+    let high = n - 1;
+    let f, g, h, i, j, m;
+    let scale;
+  
+    for (m = low + 1; m <= high - 1; m++) {
+      scale = 0;
+      for (i = m; i <= high; i++) {
+        scale = scale + Math.abs(H.get(i, m - 1));
+      }
+  
+      if (scale !== 0) {
+        h = 0;
+        for (i = high; i >= m; i--) {
+          ort[i] = H.get(i, m - 1) / scale;
+          h += ort[i] * ort[i];
+        }
+  
+        g = Math.sqrt(h);
+        if (ort[m] > 0) {
+          g = -g;
+        }
+  
+        h = h - ort[m] * g;
+        ort[m] = ort[m] - g;
+  
+        for (j = m; j < n; j++) {
+          f = 0;
+          for (i = high; i >= m; i--) {
+            f += ort[i] * H.get(i, j);
+          }
+  
+          f = f / h;
+          for (i = m; i <= high; i++) {
+            H.set(i, j, H.get(i, j) - f * ort[i]);
+          }
+        }
+  
+        for (i = 0; i <= high; i++) {
+          f = 0;
+          for (j = high; j >= m; j--) {
+            f += ort[j] * H.get(i, j);
+          }
+  
+          f = f / h;
+          for (j = m; j <= high; j++) {
+            H.set(i, j, H.get(i, j) - f * ort[j]);
+          }
+        }
+  
+        ort[m] = scale * ort[m];
+        H.set(m, m - 1, scale * g);
+      }
+    }
+  
+    for (i = 0; i < n; i++) {
+      for (j = 0; j < n; j++) {
+        V.set(i, j, i === j ? 1 : 0);
+      }
+    }
+  
+    for (m = high - 1; m >= low + 1; m--) {
+      if (H.get(m, m - 1) !== 0) {
+        for (i = m + 1; i <= high; i++) {
+          ort[i] = H.get(i, m - 1);
+        }
+  
+        for (j = m; j <= high; j++) {
+          g = 0;
+          for (i = m; i <= high; i++) {
+            g += ort[i] * V.get(i, j);
+          }
+  
+          g = g / ort[m] / H.get(m, m - 1);
+          for (i = m; i <= high; i++) {
+            V.set(i, j, V.get(i, j) + g * ort[i]);
+          }
+        }
+      }
+    }
+  }
+  
+  function hqr2(nn, e, d, V, H) {
+    let n = nn - 1;
+    let low = 0;
+    let high = nn - 1;
+    let eps = Number.EPSILON;
+    let exshift = 0;
+    let norm = 0;
+    let p = 0;
+    let q = 0;
+    let r = 0;
+    let s = 0;
+    let z = 0;
+    let iter = 0;
+    let i, j, k, l, m, t, w, x, y;
+    let ra, sa, vr, vi;
+    let notlast, cdivres;
+  
+    for (i = 0; i < nn; i++) {
+      if (i < low || i > high) {
+        d[i] = H.get(i, i);
+        e[i] = 0;
+      }
+  
+      for (j = Math.max(i - 1, 0); j < nn; j++) {
+        norm = norm + Math.abs(H.get(i, j));
+      }
+    }
+  
+    while (n >= low) {
+      l = n;
+      while (l > low) {
+        s = Math.abs(H.get(l - 1, l - 1)) + Math.abs(H.get(l, l));
+        if (s === 0) {
+          s = norm;
+        }
+        if (Math.abs(H.get(l, l - 1)) < eps * s) {
+          break;
+        }
+        l--;
+      }
+  
+      if (l === n) {
+        H.set(n, n, H.get(n, n) + exshift);
+        d[n] = H.get(n, n);
+        e[n] = 0;
+        n--;
+        iter = 0;
+      } else if (l === n - 1) {
+        w = H.get(n, n - 1) * H.get(n - 1, n);
+        p = (H.get(n - 1, n - 1) - H.get(n, n)) / 2;
+        q = p * p + w;
+        z = Math.sqrt(Math.abs(q));
+        H.set(n, n, H.get(n, n) + exshift);
+        H.set(n - 1, n - 1, H.get(n - 1, n - 1) + exshift);
+        x = H.get(n, n);
+  
+        if (q >= 0) {
+          z = p >= 0 ? p + z : p - z;
+          d[n - 1] = x + z;
+          d[n] = d[n - 1];
+          if (z !== 0) {
+            d[n] = x - w / z;
+          }
+          e[n - 1] = 0;
+          e[n] = 0;
+          x = H.get(n, n - 1);
+          s = Math.abs(x) + Math.abs(z);
+          p = x / s;
+          q = z / s;
+          r = Math.sqrt(p * p + q * q);
+          p = p / r;
+          q = q / r;
+  
+          for (j = n - 1; j < nn; j++) {
+            z = H.get(n - 1, j);
+            H.set(n - 1, j, q * z + p * H.get(n, j));
+            H.set(n, j, q * H.get(n, j) - p * z);
+          }
+  
+          for (i = 0; i <= n; i++) {
+            z = H.get(i, n - 1);
+            H.set(i, n - 1, q * z + p * H.get(i, n));
+            H.set(i, n, q * H.get(i, n) - p * z);
+          }
+  
+          for (i = low; i <= high; i++) {
+            z = V.get(i, n - 1);
+            V.set(i, n - 1, q * z + p * V.get(i, n));
+            V.set(i, n, q * V.get(i, n) - p * z);
+          }
+        } else {
+          d[n - 1] = x + p;
+          d[n] = x + p;
+          e[n - 1] = z;
+          e[n] = -z;
+        }
+  
+        n = n - 2;
+        iter = 0;
+      } else {
+        x = H.get(n, n);
+        y = 0;
+        w = 0;
+        if (l < n) {
+          y = H.get(n - 1, n - 1);
+          w = H.get(n, n - 1) * H.get(n - 1, n);
+        }
+  
+        if (iter === 10) {
+          exshift += x;
+          for (i = low; i <= n; i++) {
+            H.set(i, i, H.get(i, i) - x);
+          }
+          s = Math.abs(H.get(n, n - 1)) + Math.abs(H.get(n - 1, n - 2));
+          x = y = 0.75 * s;
+          w = -0.4375 * s * s;
+        }
+  
+        if (iter === 30) {
+          s = (y - x) / 2;
+          s = s * s + w;
+          if (s > 0) {
+            s = Math.sqrt(s);
+            if (y < x) {
+              s = -s;
+            }
+            s = x - w / ((y - x) / 2 + s);
+            for (i = low; i <= n; i++) {
+              H.set(i, i, H.get(i, i) - s);
+            }
+            exshift += s;
+            x = y = w = 0.964;
+          }
+        }
+  
+        iter = iter + 1;
+  
+        m = n - 2;
+        while (m >= l) {
+          z = H.get(m, m);
+          r = x - z;
+          s = y - z;
+          p = (r * s - w) / H.get(m + 1, m) + H.get(m, m + 1);
+          q = H.get(m + 1, m + 1) - z - r - s;
+          r = H.get(m + 2, m + 1);
+          s = Math.abs(p) + Math.abs(q) + Math.abs(r);
+          p = p / s;
+          q = q / s;
+          r = r / s;
+          if (m === l) {
+            break;
+          }
+          if (
+            Math.abs(H.get(m, m - 1)) * (Math.abs(q) + Math.abs(r)) <
+            eps *
+              (Math.abs(p) *
+                (Math.abs(H.get(m - 1, m - 1)) +
+                  Math.abs(z) +
+                  Math.abs(H.get(m + 1, m + 1))))
+          ) {
+            break;
+          }
+          m--;
+        }
+  
+        for (i = m + 2; i <= n; i++) {
+          H.set(i, i - 2, 0);
+          if (i > m + 2) {
+            H.set(i, i - 3, 0);
+          }
+        }
+  
+        for (k = m; k <= n - 1; k++) {
+          notlast = k !== n - 1;
+          if (k !== m) {
+            p = H.get(k, k - 1);
+            q = H.get(k + 1, k - 1);
+            r = notlast ? H.get(k + 2, k - 1) : 0;
+            x = Math.abs(p) + Math.abs(q) + Math.abs(r);
+            if (x !== 0) {
+              p = p / x;
+              q = q / x;
+              r = r / x;
+            }
+          }
+  
+          if (x === 0) {
+            break;
+          }
+  
+          s = Math.sqrt(p * p + q * q + r * r);
+          if (p < 0) {
+            s = -s;
+          }
+  
+          if (s !== 0) {
+            if (k !== m) {
+              H.set(k, k - 1, -s * x);
+            } else if (l !== m) {
+              H.set(k, k - 1, -H.get(k, k - 1));
+            }
+  
+            p = p + s;
+            x = p / s;
+            y = q / s;
+            z = r / s;
+            q = q / p;
+            r = r / p;
+  
+            for (j = k; j < nn; j++) {
+              p = H.get(k, j) + q * H.get(k + 1, j);
+              if (notlast) {
+                p = p + r * H.get(k + 2, j);
+                H.set(k + 2, j, H.get(k + 2, j) - p * z);
+              }
+  
+              H.set(k, j, H.get(k, j) - p * x);
+              H.set(k + 1, j, H.get(k + 1, j) - p * y);
+            }
+  
+            for (i = 0; i <= Math.min(n, k + 3); i++) {
+              p = x * H.get(i, k) + y * H.get(i, k + 1);
+              if (notlast) {
+                p = p + z * H.get(i, k + 2);
+                H.set(i, k + 2, H.get(i, k + 2) - p * r);
+              }
+  
+              H.set(i, k, H.get(i, k) - p);
+              H.set(i, k + 1, H.get(i, k + 1) - p * q);
+            }
+  
+            for (i = low; i <= high; i++) {
+              p = x * V.get(i, k) + y * V.get(i, k + 1);
+              if (notlast) {
+                p = p + z * V.get(i, k + 2);
+                V.set(i, k + 2, V.get(i, k + 2) - p * r);
+              }
+  
+              V.set(i, k, V.get(i, k) - p);
+              V.set(i, k + 1, V.get(i, k + 1) - p * q);
+            }
+          }
+        }
+      }
+    }
+  
+    if (norm === 0) {
+      return;
+    }
+  
+    for (n = nn - 1; n >= 0; n--) {
+      p = d[n];
+      q = e[n];
+  
+      if (q === 0) {
+        l = n;
+        H.set(n, n, 1);
+        for (i = n - 1; i >= 0; i--) {
+          w = H.get(i, i) - p;
+          r = 0;
+          for (j = l; j <= n; j++) {
+            r = r + H.get(i, j) * H.get(j, n);
+          }
+  
+          if (e[i] < 0) {
+            z = w;
+            s = r;
+          } else {
+            l = i;
+            if (e[i] === 0) {
+              H.set(i, n, w !== 0 ? -r / w : -r / (eps * norm));
+            } else {
+              x = H.get(i, i + 1);
+              y = H.get(i + 1, i);
+              q = (d[i] - p) * (d[i] - p) + e[i] * e[i];
+              t = (x * s - z * r) / q;
+              H.set(i, n, t);
+              H.set(
+                i + 1,
+                n,
+                Math.abs(x) > Math.abs(z) ? (-r - w * t) / x : (-s - y * t) / z,
+              );
+            }
+  
+            t = Math.abs(H.get(i, n));
+            if (eps * t * t > 1) {
+              for (j = i; j <= n; j++) {
+                H.set(j, n, H.get(j, n) / t);
+              }
+            }
+          }
+        }
+      } else if (q < 0) {
+        l = n - 1;
+  
+        if (Math.abs(H.get(n, n - 1)) > Math.abs(H.get(n - 1, n))) {
+          H.set(n - 1, n - 1, q / H.get(n, n - 1));
+          H.set(n - 1, n, -(H.get(n, n) - p) / H.get(n, n - 1));
+        } else {
+          cdivres = cdiv(0, -H.get(n - 1, n), H.get(n - 1, n - 1) - p, q);
+          H.set(n - 1, n - 1, cdivres[0]);
+          H.set(n - 1, n, cdivres[1]);
+        }
+  
+        H.set(n, n - 1, 0);
+        H.set(n, n, 1);
+        for (i = n - 2; i >= 0; i--) {
+          ra = 0;
+          sa = 0;
+          for (j = l; j <= n; j++) {
+            ra = ra + H.get(i, j) * H.get(j, n - 1);
+            sa = sa + H.get(i, j) * H.get(j, n);
+          }
+  
+          w = H.get(i, i) - p;
+  
+          if (e[i] < 0) {
+            z = w;
+            r = ra;
+            s = sa;
+          } else {
+            l = i;
+            if (e[i] === 0) {
+              cdivres = cdiv(-ra, -sa, w, q);
+              H.set(i, n - 1, cdivres[0]);
+              H.set(i, n, cdivres[1]);
+            } else {
+              x = H.get(i, i + 1);
+              y = H.get(i + 1, i);
+              vr = (d[i] - p) * (d[i] - p) + e[i] * e[i] - q * q;
+              vi = (d[i] - p) * 2 * q;
+              if (vr === 0 && vi === 0) {
+                vr =
+                  eps *
+                  norm *
+                  (Math.abs(w) +
+                    Math.abs(q) +
+                    Math.abs(x) +
+                    Math.abs(y) +
+                    Math.abs(z));
+              }
+              cdivres = cdiv(
+                x * r - z * ra + q * sa,
+                x * s - z * sa - q * ra,
+                vr,
+                vi,
+              );
+              H.set(i, n - 1, cdivres[0]);
+              H.set(i, n, cdivres[1]);
+              if (Math.abs(x) > Math.abs(z) + Math.abs(q)) {
+                H.set(
+                  i + 1,
+                  n - 1,
+                  (-ra - w * H.get(i, n - 1) + q * H.get(i, n)) / x,
+                );
+                H.set(
+                  i + 1,
+                  n,
+                  (-sa - w * H.get(i, n) - q * H.get(i, n - 1)) / x,
+                );
+              } else {
+                cdivres = cdiv(
+                  -r - y * H.get(i, n - 1),
+                  -s - y * H.get(i, n),
+                  z,
+                  q,
+                );
+                H.set(i + 1, n - 1, cdivres[0]);
+                H.set(i + 1, n, cdivres[1]);
+              }
+            }
+  
+            t = Math.max(Math.abs(H.get(i, n - 1)), Math.abs(H.get(i, n)));
+            if (eps * t * t > 1) {
+              for (j = i; j <= n; j++) {
+                H.set(j, n - 1, H.get(j, n - 1) / t);
+                H.set(j, n, H.get(j, n) / t);
+              }
+            }
+          }
+        }
+      }
+    }
+  
+    for (i = 0; i < nn; i++) {
+      if (i < low || i > high) {
+        for (j = i; j < nn; j++) {
+          V.set(i, j, H.get(i, j));
+        }
+      }
+    }
+  
+    for (j = nn - 1; j >= low; j--) {
+      for (i = low; i <= high; i++) {
+        z = 0;
+        for (k = low; k <= Math.min(j, high); k++) {
+          z = z + V.get(i, k) * H.get(k, j);
+        }
+        V.set(i, j, z);
+      }
+    }
+  }
+  
+  function cdiv(xr, xi, yr, yi) {
+    let r, d;
+    if (Math.abs(yr) > Math.abs(yi)) {
+      r = yi / yr;
+      d = yr + r * yi;
+      return [(xr + r * xi) / d, (xi - r * xr) / d];
+    } else {
+      r = yr / yi;
+      d = yi + r * yr;
+      return [(r * xr + xi) / d, (r * xi - xr) / d];
+    }
+  }
+  
+  class CholeskyDecomposition {
+    constructor(value) {
+      value = WrapperMatrix2D.checkMatrix(value);
+      if (!value.isSymmetric()) {
+        throw new Error('Matrix is not symmetric');
+      }
+  
+      let a = value;
+      let dimension = a.rows;
+      let l = new Matrix(dimension, dimension);
+      let positiveDefinite = true;
+      let i, j, k;
+  
+      for (j = 0; j < dimension; j++) {
+        let d = 0;
+        for (k = 0; k < j; k++) {
+          let s = 0;
+          for (i = 0; i < k; i++) {
+            s += l.get(k, i) * l.get(j, i);
+          }
+          s = (a.get(j, k) - s) / l.get(k, k);
+          l.set(j, k, s);
+          d = d + s * s;
+        }
+  
+        d = a.get(j, j) - d;
+  
+        positiveDefinite &= d > 0;
+        l.set(j, j, Math.sqrt(Math.max(d, 0)));
+        for (k = j + 1; k < dimension; k++) {
+          l.set(j, k, 0);
+        }
+      }
+  
+      this.L = l;
+      this.positiveDefinite = Boolean(positiveDefinite);
+    }
+  
+    isPositiveDefinite() {
+      return this.positiveDefinite;
+    }
+  
+    solve(value) {
+      value = WrapperMatrix2D.checkMatrix(value);
+  
+      let l = this.L;
+      let dimension = l.rows;
+  
+      if (value.rows !== dimension) {
+        throw new Error('Matrix dimensions do not match');
+      }
+      if (this.isPositiveDefinite() === false) {
+        throw new Error('Matrix is not positive definite');
+      }
+  
+      let count = value.columns;
+      let B = value.clone();
+      let i, j, k;
+  
+      for (k = 0; k < dimension; k++) {
+        for (j = 0; j < count; j++) {
+          for (i = 0; i < k; i++) {
+            B.set(k, j, B.get(k, j) - B.get(i, j) * l.get(k, i));
+          }
+          B.set(k, j, B.get(k, j) / l.get(k, k));
+        }
+      }
+  
+      for (k = dimension - 1; k >= 0; k--) {
+        for (j = 0; j < count; j++) {
+          for (i = k + 1; i < dimension; i++) {
+            B.set(k, j, B.get(k, j) - B.get(i, j) * l.get(i, k));
+          }
+          B.set(k, j, B.get(k, j) / l.get(k, k));
+        }
+      }
+  
+      return B;
+    }
+  
+    get lowerTriangularMatrix() {
+      return this.L;
+    }
+  }
+  
+  class nipals {
+    constructor(X, options = {}) {
+      X = WrapperMatrix2D.checkMatrix(X);
+      let { Y } = options;
+      const {
+        scaleScores = false,
+        maxIterations = 1000,
+        terminationCriteria = 1e-10,
+      } = options;
+  
+      let u;
+      if (Y) {
+        if (isAnyArray.isAnyArray(Y) && typeof Y[0] === 'number') {
+          Y = Matrix.columnVector(Y);
+        } else {
+          Y = WrapperMatrix2D.checkMatrix(Y);
+        }
+        if (Y.rows !== X.rows) {
+          throw new Error('Y should have the same number of rows as X');
+        }
+        u = Y.getColumnVector(0);
+      } else {
+        u = X.getColumnVector(0);
+      }
+  
+      let diff = 1;
+      let t, q, w, tOld;
+  
+      for (
+        let counter = 0;
+        counter < maxIterations && diff > terminationCriteria;
+        counter++
+      ) {
+        w = X.transpose().mmul(u).div(u.transpose().mmul(u).get(0, 0));
+        w = w.div(w.norm());
+  
+        t = X.mmul(w).div(w.transpose().mmul(w).get(0, 0));
+  
+        if (counter > 0) {
+          diff = t.clone().sub(tOld).pow(2).sum();
+        }
+        tOld = t.clone();
+  
+        if (Y) {
+          q = Y.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
+          q = q.div(q.norm());
+  
+          u = Y.mmul(q).div(q.transpose().mmul(q).get(0, 0));
+        } else {
+          u = t;
+        }
+      }
+  
+      if (Y) {
+        let p = X.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
+        p = p.div(p.norm());
+        let xResidual = X.clone().sub(t.clone().mmul(p.transpose()));
+        let residual = u.transpose().mmul(t).div(t.transpose().mmul(t).get(0, 0));
+        let yResidual = Y.clone().sub(
+          t.clone().mulS(residual.get(0, 0)).mmul(q.transpose()),
+        );
+  
+        this.t = t;
+        this.p = p.transpose();
+        this.w = w.transpose();
+        this.q = q;
+        this.u = u;
+        this.s = t.transpose().mmul(t);
+        this.xResidual = xResidual;
+        this.yResidual = yResidual;
+        this.betas = residual;
+      } else {
+        this.w = w.transpose();
+        this.s = t.transpose().mmul(t).sqrt();
+        if (scaleScores) {
+          this.t = t.clone().div(this.s.get(0, 0));
+        } else {
+          this.t = t;
+        }
+        this.xResidual = X.sub(t.mmul(w.transpose()));
+      }
+    }
+  }
+  
+  exports.AbstractMatrix = AbstractMatrix;
+  exports.CHO = CholeskyDecomposition;
+  exports.CholeskyDecomposition = CholeskyDecomposition;
+  exports.EVD = EigenvalueDecomposition;
+  exports.EigenvalueDecomposition = EigenvalueDecomposition;
+  exports.LU = LuDecomposition;
+  exports.LuDecomposition = LuDecomposition;
+  exports.Matrix = Matrix;
+  exports.MatrixColumnSelectionView = MatrixColumnSelectionView;
+  exports.MatrixColumnView = MatrixColumnView;
+  exports.MatrixFlipColumnView = MatrixFlipColumnView;
+  exports.MatrixFlipRowView = MatrixFlipRowView;
+  exports.MatrixRowSelectionView = MatrixRowSelectionView;
+  exports.MatrixRowView = MatrixRowView;
+  exports.MatrixSelectionView = MatrixSelectionView;
+  exports.MatrixSubView = MatrixSubView;
+  exports.MatrixTransposeView = MatrixTransposeView;
+  exports.NIPALS = nipals;
+  exports.Nipals = nipals;
+  exports.QR = QrDecomposition;
+  exports.QrDecomposition = QrDecomposition;
+  exports.SVD = SingularValueDecomposition;
+  exports.SingularValueDecomposition = SingularValueDecomposition;
+  exports.WrapperMatrix1D = WrapperMatrix1D;
+  exports.WrapperMatrix2D = WrapperMatrix2D;
+  exports.correlation = correlation;
+  exports.covariance = covariance;
+  exports["default"] = Matrix;
+  exports.determinant = determinant;
+  exports.inverse = inverse;
+  exports.linearDependencies = linearDependencies;
+  exports.pseudoInverse = pseudoInverse;
+  exports.solve = solve;
+  exports.wrap = wrap;
+  
+  },{"is-any-array":2,"ml-array-rescale":5}],7:[function(require,module,exports){
+  "use strict";
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.RegressionError = exports.PolynomialFeatures = exports.PolynomialRegressor = void 0;
+  var polynomial_regression_1 = require("./polynomial-regression");
+  Object.defineProperty(exports, "PolynomialRegressor", { enumerable: true, get: function () { return polynomial_regression_1.PolynomialRegressor; } });
+  var polynomial_features_1 = require("./polynomial-features");
+  Object.defineProperty(exports, "PolynomialFeatures", { enumerable: true, get: function () { return polynomial_features_1.PolynomialFeatures; } });
+  var util_1 = require("./util/util");
+  Object.defineProperty(exports, "RegressionError", { enumerable: true, get: function () { return util_1.RegressionError; } });
+  
+  },{"./polynomial-features":8,"./polynomial-regression":9,"./util/util":11}],8:[function(require,module,exports){
+  "use strict";
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.PolynomialFeatures = void 0;
+  const itertools_1 = require("./util/itertools");
+  const util_1 = require("./util/util");
+  /**
+   * Transforms feature vectors to vectors of certain monomials thereof.
+   *
+   * E.g. after setting options (degree, homogeneous, interactionOnly), and
+   * *fit()*ing, *transform()*s lists of feature vectors like [a, b, c], where a,
+   * b and c are numbers, into the following possible outputs
+   *
+   * - [a^2, ba, ca, a, b^2, cb, b, c^2, c, 1] (degree = 2), that is, all
+   *   monomials in a, b, c up to degree two.
+   *
+   * - [a^2, ba, ca, b^2, cb, c^2] (degree = 2, homogeneous = true), that is, all
+   *   monomials in a, b, c of degree exactly equal to two.
+   *
+   * - [ab, ac, bc] (degree = 2, homogeneous = true, interactionOnly = true), that
+   *   is, all monomials in a, b, c of degree exactly equal to two and no feature
+   *   raised to a power larger than one.
+   *
+   * - [ab, ac, a, bc, b, c, 1] (degree = 2, homogeneous = false, interactionOnly
+   *   = true), that is, all monomials in a, b, c of degree at most two and no
+   *   feature raised to a power larger than one.
+   *
+   * Of course, the number of features is not restricted to three, but must be the
+   * same in each feature vector of the list to be *transformed()*.
+   */
+  class PolynomialFeatures {
+      /**
+       * Basic configuration. You can skip configuration by providing no arguments.
+       *
+       * In case no arguments are given you have to use the method fromConfig(...)
+       * to set up a configuration in order to use the methods fit(...),
+       * transform(...) and fitTransform(...).
+       *
+       * @param degree Highest order of the monomials
+       * @param homogeneous Whether to include only highest order monomials
+       * @param interactionOnly Whether to disallow higher powers of single features
+       */
+      constructor(degree, homogeneous = false, interactionOnly = false) {
+          if (degree !== undefined) {
+              this._degree = degree;
+              this._homogeneous = homogeneous;
+              this._interactionOnly = interactionOnly;
+          }
+      }
+      /** Configuration option 'degree' */
+      get degree() {
+          return this._degree;
+      }
+      /** Configuration option 'homogenous' */
+      get homogenous() {
+          return this._homogeneous;
+      }
+      /** Configuration option 'interactionOnly' */
+      get interactionOnly() {
+          return this._interactionOnly;
+      }
+      /**
+       * Number of input features.
+       *
+       * The only configuration option which is set by the fit method.
+       */
+      get nFeaturesIn() {
+          return this._nFeaturesIn;
+      }
+      /** Saves configuration to a simple option-bag.
+       *
+       * The configuration specifies the internal state of PolynomialFeatures
+       * completely. Hence the config of the transformer can be used to save it to a
+       * file.
+      */
+      get config() {
+          return { degree: this.degree, homogeneous: this.homogenous,
+              interactionOnly: this.interactionOnly, nFeaturesIn: this.nFeaturesIn };
+      }
+      /** Loads configuration from simple option-bag. */
+      fromConfig(config) {
+          this._degree = config.degree;
+          this._homogeneous = config.homogeneous;
+          this._interactionOnly = config.interactionOnly;
+          ;
+          this._nFeaturesIn = config.nFeaturesIn;
+          this.assertValidConfig();
+      }
+      /** Sets the number of input features.
+       *
+       * Call this method before transform(...). Afterwards the transform(...)
+       * method will only accept input with the correct number of features.
+      */
+      fit(x) {
+          this._nFeaturesIn = x[0].length;
+          this.assertValidConfig(); // After fitting we should have a valid config
+      }
+      /**
+       * Returns the list of polynomial features corresponding to x.
+       *
+       * @param x List of features vectors (with same number of features each).
+       */
+      transform(x) {
+          let y = [];
+          for (const xi of x) {
+              if (xi.length !== this.nFeaturesIn) {
+                  let message = `Invalid input: Input dimension is ${xi.length} expected ${this.nFeaturesIn}.`;
+                  if (!this.nFeaturesIn)
+                      message += ' Maybe you forgot to fit(...) the data.';
+                  throw new util_1.RegressionError(message);
+              }
+              let yi = [];
+              const ximod = this.homogenous || this.interactionOnly ? xi.concat() : xi.concat([1]);
+              const degrees = this.interactionOnly && !this.homogenous ? [...Array(this.degree + 1).keys()] : [this.degree];
+              const combis = this._interactionOnly ? itertools_1.combinations : itertools_1.combinationsWithRepitition;
+              for (const degree of degrees) {
+                  for (const comb of combis(ximod, degree)) {
+                      yi.push(comb.reduce((p, c) => p *= c, 1));
+                  }
+              }
+              y.push(yi);
+          }
+          return y;
+      }
+      /**
+       * Same as applying first fit(...) and then transform(...) to x.
+       *
+       * @param x List of features vectors.
+       */
+      fitTransform(x) {
+          this.fit(x);
+          return this.transform(x);
+      }
+      /**
+       * Throw error in case of invalid configuration.
+       */
+      assertValidConfig() {
+          if (this._degree === undefined || this._homogeneous === undefined || this._interactionOnly === undefined
+              || this._nFeaturesIn === undefined) {
+              throw new util_1.RegressionError("Incomplete configuration not allowed");
+          }
+          if (this.degree < 0) {
+              throw new util_1.RegressionError("Degree not allowed to be negative");
+          }
+          if (this.degree % 1 !== 0) {
+              throw new util_1.RegressionError("Degree must be integer");
+          }
+          if (this.interactionOnly && this.degree > this.nFeaturesIn) {
+              throw new util_1.RegressionError("Degree cannot exceed number of input features in interactionOnly-mode");
+          }
+          if (this.nFeaturesIn % 1 !== 0) {
+              throw new util_1.RegressionError("Number of input features must be integer");
+          }
+          if (this.nFeaturesIn < 0) {
+              throw new util_1.RegressionError("Number of input features cannot be negative");
+          }
+      }
+  }
+  exports.PolynomialFeatures = PolynomialFeatures;
+  
+  },{"./util/itertools":10,"./util/util":11}],9:[function(require,module,exports){
+  "use strict";
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.PolynomialRegressor = void 0;
+  const ml_matrix_1 = require("ml-matrix");
+  const polynomial_features_1 = require("./polynomial-features");
+  /**
+   * Model for performing multivariate polynomial regression.
+   *
+   * Train the model by *fit()*ing it on data available for training. Afterwards
+   * *predict()*ion is possible.
+   *
+   * The model is implemented as a pipe consisting of two steps. First the input
+   * is transformed by the class PolynomialFeatures, this reduces the problem to a
+   * *linear* regression problem. Hence in the second step we simply apply linear
+   * regression.
+   */
+  class PolynomialRegressor {
+      /**
+       * Basic configuration. You can skip configuration by providing no arguments.
+       *
+       * In case no arguments are given you have to use the method fromConfig(...)
+       * to set up a configuration in order to use the method fit(...).
+       *
+       * For the exact meaning of the arguments see the documentation of
+       * PolynomialFeatures.
+       *
+       * @param degree Highest order of the monomials
+       * @param homogeneous Whether to include only highest order monomials
+       * @param interactionOnly Whether to disallow higher powers of single features
+       */
+      constructor(degree, homogeneous = false, interactionOnly = false) {
+          if (degree !== undefined) {
+              this._polyFeatures = new polynomial_features_1.PolynomialFeatures(degree, homogeneous, interactionOnly);
+          }
+      }
+      /** Number of input features. */
+      get nFeaturesIn() {
+          return this._polyFeatures.nFeaturesIn;
+      }
+      /** Number of output features. */
+      get nFeaturesOut() {
+          return this._weights ? this._weights[0].length : undefined;
+      }
+      /** The weight matrix of the underlying linear regression model. */
+      get weights() {
+          return this._weights;
+      }
+      /** Instance of PolynomialFeatures responsible for transforming the input. */
+      get polyFeatures() {
+          return this._polyFeatures;
+      }
+      /**
+       * Trains the model.
+       *
+       * @param x List of input vectors.
+       * @param y List of corresponding desired output vectors.
+       */
+      fit(x, y) {
+          let xpoly = this._polyFeatures.fitTransform(x);
+          this._weights = new ml_matrix_1.SVD(xpoly, { autoTranspose: true }).solve(new ml_matrix_1.Matrix(y)).to2DArray();
+      }
+      /**
+       * Make predictions.
+       *
+       * @param x List of inputs.
+       */
+      predict(x) {
+          const xpoly = this._polyFeatures.transform(x);
+          let y = [];
+          for (let i = 0; i < x.length; ++i) {
+              y.push(this.predictPoly(xpoly[i]));
+          }
+          return y;
+      }
+      /**
+       * Saves configuration to a simple option-bag
+       *
+       * The configuration specifies the internal state of a PolynomialRegressor
+       * completely. Hence the config of a fitted model can be used to save the
+       * model to a file.
+      */
+      get config() {
+          return { weights: this.weights, polyFeatures: this.polyFeatures ? this.polyFeatures.config : undefined };
+      }
+      /** Loads configuration from simple option-bag.
+       *
+       * @param config The configuration to load from.
+       */
+      fromConfig(config) {
+          this._weights = config.weights;
+          this._polyFeatures = new polynomial_features_1.PolynomialFeatures();
+          this._polyFeatures.fromConfig(config.polyFeatures);
+      }
+      /** For internal use only. */
+      predictPoly(xpoly) {
+          let y = Array.from(Array(this.nFeaturesOut), () => 0);
+          for (let i = 0; i < xpoly.length; ++i) {
+              for (let j = 0; j < this.nFeaturesOut; ++j) {
+                  y[j] += this._weights[i][j] * xpoly[i];
+              }
+          }
+          return y;
+      }
+  }
+  exports.PolynomialRegressor = PolynomialRegressor;
+  
+  },{"./polynomial-features":8,"ml-matrix":6}],10:[function(require,module,exports){
+  "use strict";
+  /** Basic combinatorial utility. For internal use only. */
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.combinations = exports.combinationsWithRepitition = void 0;
+  /**
+   * Yield all k-combinations with repition from a given sequence.
+   *
+   * For example if the iterable is the array [1,2,3,4] and k = 2 the resulting
+   * generator yields all multi-subsequences of length 2:
+   *
+   *   [1,1], [2,1], [3,1], [4,1], [2,2], [3,2], [4,2], [3,3], [4,3], [4,4].
+   *
+   * The elements are ordered lexicographically.
+   *
+   * Remark: If n is the length of the iterable, there are n+k-1 over k such
+   * combinations.
+   *
+   * See also:
+   * https://en.wikipedia.org/wiki/Combination#Number_of_combinations_with_repetition
+   *
+   * @param iterable A finite sequence
+   * @param k The length of the combinations: 1, 2, 3, ...
+   */
+  function* combinationsWithRepitition(iterable, k) {
+      if (k < 0)
+          return [];
+      const pool = Array.from(iterable);
+      const maxi = pool.length - 1;
+      let indices = Array.from(Array(k), () => 0);
+      let next = Array.from(Array(k), () => pool[0]);
+      yield next;
+      outerLoop: while (true) {
+          let n = 0;
+          while (indices[n] === maxi) {
+              ++n;
+          }
+          if (n === k)
+              break outerLoop;
+          const current = indices[n] += 1;
+          next[n] = pool[current];
+          while (--n >= 0) {
+              indices[n] = current;
+              next[n] = pool[current];
+          }
+          yield next;
+      }
+  }
+  exports.combinationsWithRepitition = combinationsWithRepitition;
+  /**
+   * Yield all k-combinations from a given sequence .
+   *
+   * For example if the iterable is the array [1,2,3,4] and k = 2 the resulting
+   * generator yields all subsequences of length 2:
+   *
+   *   [1,2], [1,3], [1,4], [2,3], [2,4], [3,4].
+   *
+   * The ordering is given as follows: Start with the k-combination with minimal
+   * indices (here [1,2]), then repeat the following: Generate the next
+   * combination by selecting the right-most index which can be incremented by 1
+   * without making the new combination invalid (and do that), until this is
+   * impossible (then all possible k-combinations are visited).
+   *
+   * Return null in case k > length of iterable
+   *
+   * Remark: If n is the length of iterable, the number of generated
+   * k-combinations is given by the binomal coefficient n over k.
+   *
+   * See also:
+   * https://en.wikipedia.org/wiki/Combination#Enumerating_k-combinations
+   *
+   * @param iterable A finite sequence
+   * @param k The length of the combinations: 1, 2, 3, ...
+   */
+  function* combinations(iterable, k) {
+      const pool = Array.from(iterable);
+      if (k > pool.length || k < 0)
+          return [];
+      let indices = Array.from({ length: k }, (_, k) => k);
+      let next = Array.from({ length: k }, (_, k) => pool[k]);
+      yield next;
+      while (true) {
+          let n = k - 1;
+          while (indices[n] === pool.length - k + n) {
+              --n;
+          }
+          ;
+          if (n === -1)
+              break;
+          ++indices[n];
+          next[n] = pool[indices[n]];
+          while (++n < k) {
+              indices[n] = indices[n - 1] + 1;
+              next[n] = pool[indices[n]];
+          }
+          yield next;
+      }
+  }
+  exports.combinations = combinations;
+  
+  },{}],11:[function(require,module,exports){
+  "use strict";
+  Object.defineProperty(exports, "__esModule", { value: true });
+  exports.RegressionError = void 0;
+  class RegressionError extends Error {
+  }
+  exports.RegressionError = RegressionError;
+  
+  },{}]},{},[1]);
+  
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/utes/getFile.js b/system/javascript/osapjs/client/utes/getFile.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8186446ee4e2ea793d1fcb792550e2d1a945732
--- /dev/null
+++ b/system/javascript/osapjs/client/utes/getFile.js
@@ -0,0 +1,15 @@
+// bundled up file getter 
+let GetFile = (file) => {
+  return new Promise((resolve, reject) => {
+    $.ajax({
+      type: "GET",
+      url: file,
+      error: function () { reject(`req for ${file} fails`) },
+      success: function (xhr, statusText) {
+        resolve(xhr)
+      }
+    })
+  })
+}
+
+export { GetFile }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/utes/lsq.js b/system/javascript/osapjs/client/utes/lsq.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b4ab3a54d64cb8d0473baff403a953b4c53fdeb
--- /dev/null
+++ b/system/javascript/osapjs/client/utes/lsq.js
@@ -0,0 +1,45 @@
+/*
+
+lsq.js 
+
+input previous system measurements as state (lists)
+make predictions for y based on input at x, with lsq. from old data
+
+*/
+
+import smallmath from './smallmath.js'
+
+export default function LeastSquares() {
+  // internal state 
+  // unit observation to start 
+  let observations = []
+  let m = 1 
+  let b = 0
+
+  // setup 
+  this.setObservations = (xy) => {
+    observations = JSON.parse(JSON.stringify(xy))
+    if(observations[0].length > 2){
+      let lsqr = smallmath.lsq(observations[0], observations[1])
+      m = lsqr.m 
+      b = lsqr.b 
+      //console.log(m, b)
+    }
+  }
+
+  // to generate human-readable interp of model 
+  this.printFunction = () => {
+    if (b >= 0) {
+      return `${m.toExponential(2)} x + ${b.toExponential(2)}`
+    } else {
+      return `${m.toExponential(2)} x ${b.toExponential(2)}`
+    }
+  }
+
+  this.predict = (x) => {
+    return m * x + b 
+  }
+
+  // start with 
+  this.setObservations([[1,2,3], [1,2,3]])
+}
diff --git a/system/javascript/osapjs/client/utes/saveFile.js b/system/javascript/osapjs/client/utes/saveFile.js
new file mode 100644
index 0000000000000000000000000000000000000000..b314bb6b62d07a5fa3a4750a6df18588e1191036
--- /dev/null
+++ b/system/javascript/osapjs/client/utes/saveFile.js
@@ -0,0 +1,24 @@
+let SaveFile = (obj, format, name) => {
+    // serialize the thing
+    let url = null
+    if(format == 'json'){
+        url = URL.createObjectURL(new Blob([JSON.stringify(obj, null, 2)], {
+            type: "application/json"
+        }))
+    } else if (format == 'csv') {
+        let csvContent = "data:text/csv;charset=utf-8," //+ obj.map(e => e.join(',')).join('\n')
+        csvContent += "Time,Extension,Load\n"
+        csvContent += "(sec),(mm),(N)\n"
+        for(let line of obj){
+            csvContent += `${line[0].toFixed(3)},${line[1].toFixed(4)},${line[2].toFixed(4)}\n`
+        }
+        console.log(csvContent)
+        url = encodeURI(csvContent)
+    }
+    // hack to trigger the download,
+    let anchor = $('<a>ok</a>').attr('href', url).attr('download', name + `.${format}`).get(0)
+    $(document.body).append(anchor)
+    anchor.click()
+}
+
+export { SaveFile }
\ No newline at end of file
diff --git a/system/javascript/osapjs/client/utes/smallmath.js b/system/javascript/osapjs/client/utes/smallmath.js
new file mode 100644
index 0000000000000000000000000000000000000000..c85a8adb80eadede6c6ec727f97a80c917fd5fba
--- /dev/null
+++ b/system/javascript/osapjs/client/utes/smallmath.js
@@ -0,0 +1,76 @@
+// least squares from https://medium.com/@sahirnambiar/linear-least-squares-a-javascript-implementation-and-a-definitional-question-e3fba55a6d4b
+// mod to return object of m, b, values.x, values.y,
+var smallmath = {
+  lsq: function(values_x, values_y) {
+    var x_sum = 0;
+    var y_sum = 0;
+    var xy_sum = 0;
+    var xx_sum = 0;
+    var count = 0;
+
+    /*
+     * The above is just for quick access, makes the program faster
+     */
+    var x = 0;
+    var y = 0;
+    var values_length = values_x.length;
+
+    if (values_length != values_y.length) {
+      throw new Error('The parameters values_x and values_y need to have same size!');
+    }
+
+    /*
+     * Above and below cover edge cases
+     */
+    if (values_length === 0) {
+      return [
+        [],
+        []
+      ];
+    }
+
+    /*
+     * Calculate the sum for each of the parts necessary.
+     */
+    for (let i = 0; i < values_length; i++) {
+      x = values_x[i];
+      y = values_y[i];
+      x_sum += x;
+      y_sum += y;
+      xx_sum += x * x;
+      xy_sum += x * y;
+      count++;
+    }
+
+    /*
+     * Calculate m and b for the line equation:
+     * y = x * m + b
+     */
+    var m = (count * xy_sum - x_sum * y_sum) / (count * xx_sum - x_sum * x_sum);
+    var b = (y_sum / count) - (m * x_sum) / count;
+
+    /*
+     * We then return the x and y data points according to our fit
+     */
+    var result_values_x = [];
+    var result_values_y = [];
+
+    for (let i = 0; i < values_length; i++) {
+      x = values_x[i];
+      y = x * m + b;
+      result_values_x.push(x);
+      result_values_y.push(y);
+    }
+
+    return {
+      m: m,
+      b: b,
+      values: {
+        x: result_values_x,
+        y: result_values_y
+      }
+    }
+  }
+}
+
+export default smallmath
diff --git a/system/javascript/osapjs/client/utes/smallvectors.js b/system/javascript/osapjs/client/utes/smallvectors.js
new file mode 100644
index 0000000000000000000000000000000000000000..87889789bb1ea884fddfddbd5f69c4fa891c1818
--- /dev/null
+++ b/system/javascript/osapjs/client/utes/smallvectors.js
@@ -0,0 +1,49 @@
+let vDist = (v1, v2) => {
+    // takes v1, v2 to be arrays of same length
+    // computes cartesian distance
+    var sum = 0
+    for (let i = 0; i < v1.length; i++) {
+        sum += (v1[i] - v2[i]) * (v1[i] - v2[i])
+    }
+    return Math.sqrt(sum)
+}
+
+let vSum = (v1, v2) => {
+  let ret = []
+  for(let i = 0; i < v1.length; i ++){
+    ret.push(v1[i] + v2[i])
+  }
+  return ret
+}
+
+let vLen = (v) => {
+  let sum = 0
+  for(let i = 0; i < v.length; i ++){
+    sum += Math.pow(v[i], 2)
+  }
+  return Math.sqrt(sum)
+}
+
+// from v1 to v2,
+let vUnitBetween = (v1, v2) => {
+  let dist = vDist(v1, v2)
+  let ret = []
+  for(let i = 0; i < v1.length; i ++){
+    ret[i] = (v2[i] - v1[i]) / dist
+  }
+  return ret
+}
+
+let vScalar = (v, s) => {
+  let ret = []
+  for(let i = 0; i < v.length; i ++){
+    ret[i] = v[i] * s
+  }
+  return ret
+}
+
+let deg = (rad) => {
+  return rad * (180 / Math.PI)
+}
+
+export { vDist, vSum, vLen, vUnitBetween, vScalar, deg }
diff --git a/system/javascript/osapjs/core/endpoint.js b/system/javascript/osapjs/core/endpoint.js
new file mode 100644
index 0000000000000000000000000000000000000000..4888beff1cf5634a5439856d1b349d897b87d2d9
--- /dev/null
+++ b/system/javascript/osapjs/core/endpoint.js
@@ -0,0 +1,321 @@
+/*
+osap-endpoint.js
+
+prototype software entry point / network endpoint for osap system
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS, VT, EP } from './ts.js'
+import TIME from './time.js'
+import PK from './packets.js'
+import Vertex from './vertex.js'
+
+let reverseRoute = (route) => {
+  console.error(`badness, pls refactor for pk.writeReply`)
+}
+
+export default class Endpoint extends Vertex {
+  constructor(parent, indice) {
+    super(parent, indice)
+  }
+
+  /* to implement */
+  // write this.onData(), returning promise when data is cleared out 
+  // use this.transmit(bytes), 
+  // use this.addRoute(route) to add routes 
+
+  // endpoint addnl'y has outgoing routes, 
+  routes = []
+  type = VT.ENDPOINT
+  name = "unnamed endpoint"
+
+  // and has a local data cache 
+  data = new Uint8Array(0)
+
+  // has outgoing routes, 
+  addRoute = function (route, mode = "acked") {
+    // console.log(`adding route to ep ${this.indice}`, route)
+    if (this.maxStackLength <= this.routes.length) {
+      console.warn('increasing stack space to match count of routes')
+      this.maxStackLength++
+    }
+    // endpoints store route objects that have a .mode setting,
+    // ... 
+    switch (mode) {
+      case "ackless":
+        route.mode = EP.ROUTEMODE_ACKLESS
+        break;
+      case "acked":
+      default:
+        route.mode = EP.ROUTEMODE_ACKED
+        break;
+    }
+    this.routes.push(route)
+  }
+
+  // can upd8 how long it takes to to 
+  timeoutLength = TIME.staleTimeout
+  setTimeoutLength = (time) => {
+    this.timeoutLength = time
+  }
+
+  // software data delivery, define per endpoint, 
+  // onData handlers can return promises in order to enact flow control,
+  onData = function (data) {
+    return new Promise((resolve, reject) => {
+      console.warn(`default endpoint onData at ${this.name}, dataLen is ${data.length}`)
+      resolve()
+    })
+  }
+
+  // local helper, wraps onData in always-promiseness,
+  token = false
+  onDataResolver = (data) => {
+    try {
+      let res = this.onData(data)
+      if (res instanceof Promise) {   // return user promise, 
+        return res
+      } else {                        // invent & resolve promise, 
+        return new Promise((resolve, reject) => {
+          resolve()
+        })
+      }  
+    } catch (err) {
+      console.error(`error during onData call...`, err)
+      return new Promise((resolve, reject) => {
+        resolve()
+      })
+    }
+  }
+
+  // handles 'dest' keys at endpoints, 
+  destHandler = function (item, ptr) {
+    // item.data[ptr] == PK.PTR, item.data[ptr + 1] == PK.DEST 
+    switch (item.data[ptr + 2]) {
+      case EP.SS_ACKLESS:
+        if (this.token) {
+          // packet will wait for res, 
+          return
+        } else {
+          this.token = true
+          this.onDataResolver(new Uint8Array(item.data.subarray(ptr + 3))).then(() => {
+            // resolution to the promise means data is OK, we accept 
+            this.data = new Uint8Array(item.data.subarray(ptr + 3))
+            this.token = false
+          }).catch((err) => {
+            // error / rejection means not our data, donot change internal, but clear for new 
+            this.token = false
+          })
+          item.handled(); break;
+        }
+      case EP.SS_ACKED:
+        if (this.token) {
+          // packet will wait for res, 
+          return
+        } else {
+          this.token = true
+          this.onDataResolver(new Uint8Array(item.data.subarray(ptr + 4))).then(() => {
+            this.data = new Uint8Array(item.data.subarray(ptr + 4))
+            this.token = false
+            // payload is just the dest key, ack key & id, id is at ptr + dest + key + id 
+            let datagram = PK.writeReply(item.data, new Uint8Array([PK.DEST, EP.SS_ACK, item.data[ptr + 3]]))
+            // we... should flowcontrol this, it's awkward, just send it, this is OK in JS 
+            this.handle(datagram, VT.STACK_ORIGIN)
+          }).catch((err) => {
+            this.token = false
+          })
+          item.handled(); break;
+        }
+      case EP.SS_ACK:
+        { // ack *to us* arriveth, check against awaiting transmits 
+          let ackID = item.data[ptr + 3]
+          for (let a = 0; a < this.acksAwaiting.length; a++) {
+            if (this.acksAwaiting[a].id == ackID) {
+              this.acksAwaiting.splice(a, 1)
+            }
+          }
+          if (this.acksAwaiting.length == 0) {
+            this.acksResolve()
+          }
+        }
+        item.handled(); break;
+      case EP.QUERY:
+        {
+          // new payload for reply, keys are dest, QUERY_RES, and ID from incoming, 
+          let payload = new Uint8Array(3 + this.data.length)
+          payload[0] = PK.DEST; payload[1] = EP.QUERY_RES; payload[2] = item.data[ptr + 3];
+          // write-in data,
+          payload.set(this.data, 3)
+          // formulate packet, 
+          let datagram = PK.writeReply(item.data, payload)
+          this.handle(datagram, VT.STACK_ORIGIN)
+        }
+        item.handled(); break;
+      case EP.ROUTE_QUERY_REQ:
+        {
+          // let's see about our route... it should be at 
+          let rqid = item.data[ptr + 3]
+          let indice = item.data[ptr + 4]
+          // make payloads, 
+          let payload = {}
+          if (this.routes[indice]) {
+            let route = this.routes[indice]
+            // this is dest, reply key, id, mode, + 2 <ttl> + 2 <segsize> + route.length, 
+            payload = new Uint8Array(4 + 4 + route.path.length)
+            payload.set([PK.DEST, EP.ROUTE_QUERY_RES, rqid, route.mode], 0)
+            let wptr = 4
+            wptr += TS.write('uint16', route.ttl, payload, wptr)
+            wptr += TS.write('uint16', route.segSize, payload, wptr)
+            // write the actual path in... 
+            payload.set(route.path, wptr)
+          } else {
+            // destination key, reply key, id to match, '0' to indicate no-route-here, 
+            payload = new Uint8Array([PK.DEST, EP.ROUTE_QUERY_RES, rqid, 0])
+          }
+          // format reply, wipe & replace at dest stack,
+          let datagram = PK.writeReply(item.data, payload)
+          item.handled()
+          this.handle(datagram, VT.STACK_DEST)
+        }
+        break;
+      case EP.ROUTE_SET_REQ:
+        {
+          // uuuuh 
+          let rqid = item.data[ptr + 3]
+          // the new route would be: mode, ttl, segsize, path... as in the packet, 
+          let route = {
+            mode: item.data[ptr + 4],
+            ttl: TS.read('uint16', item.data, ptr + 5),
+            segSize: TS.read('uint16', item.data, ptr + 7),
+            path: new Uint8Array(item.data.subarray(ptr + 9))
+          }
+          // add it... have infinite length in js, right? 
+          this.addRoute(route)
+          // and ack that, 1 is yes-it-worked, 0 is an error... more verbose later, maybe, haha 
+          let datagram = PK.writeReply(item.data, [PK.DEST, EP.ROUTE_SET_RES, rqid, 1])
+          item.handled()
+          this.handle(datagram, VT.STACK_DEST)
+        }
+        break;
+      case EP.ROUTE_RM_REQ:
+        {
+          // uuuuh 
+          let rqid = item.data[ptr + 3]
+          let indice = item.data[ptr + 4]
+          // either/or, 
+          let payload = new Uint8Array([PK.DEST, EP.ROUTE_RM_RES, rqid, 0])
+          // now, if we can rm, do:
+          if (this.routes[indice]) {
+            this.routes.splice(indice, 1)
+            payload[3] = 1
+          }
+          // wrip it & ship it, 
+          let datagram = PK.writeReply(item.data, payload)
+          item.handled()
+          this.handle(datagram, VT.STACK_ORIGIN)
+        }
+        break;
+      case EP.QUERY_RES:
+        // query response, 
+        console.error(`query response arrived at endpoint, should've gone to a query vt...`)
+        item.handled()
+        break;
+      default:
+        // not recognized: resolving here will cause pck to clear above 
+        console.error(`nonrec endpoint key at ep ${this.indice}`)
+        item.handled()
+        break;
+    }
+  }
+
+  runningAckID = 68
+  acksAwaiting = []
+  acksResolve = null
+
+  // this could be smarter, since we already have this acksResolve() state 
+  awaitAllAcks = (timeout = this.timeoutLength) => {
+    return new Promise((resolve, reject) => {
+      let startTime = TIME.getTimeStamp()
+      let check = () => {
+        if (this.acksAwaiting.length == 0) {
+          resolve()
+        } else if (TIME.getTimeStamp() - startTime > timeout) {
+          reject(`awaitAllAcks timeout`)
+        } else {
+          setTimeout(check, 0)
+        }
+      }
+      check()
+    })
+  }
+
+  // transmit to all routes & await return before resolving, 
+  write = async (data, mode = "ackless") => {
+    try {
+      // console.warn(`endpoint ${this.indice} writes ${mode}`)
+      // it's the uint8-s only club again, 
+      if (!(data instanceof Uint8Array)) throw new Error(`non-uint8_t write at endpoint, rejecting`);
+      // otherwise keep that data, 
+      this.data = data
+      // now wait for clear space, we need as many slots open as we have routes to write to, 
+      await this.awaitStackAvailableSpace(VT.STACK_ORIGIN, this.timeoutLength, this.routes.length)
+      // now we can write our datagrams, yeah ?
+      if (mode == "ackless") {
+        for (let route of this.routes) {
+          // this is data length + 1 (DEST) + 1 (EP_SSEG_ACKLESS)
+          let payload = new Uint8Array(this.data.length + 2)
+          payload[0] = PK.DEST
+          payload[1] = EP.SS_ACKLESS
+          payload.set(this.data, 2)
+          // the whole gram, and uptake... 
+          let datagram = PK.writeDatagram(route, payload)
+          this.handle(datagram, VT.STACK_ORIGIN)
+        } // that's it, ackless write is done, async will complete, 
+      } else if (mode == "acked") {
+        // wait to have zero previous acks awaiting... right ? 
+        await this.awaitAllAcks()
+        // now write 'em 
+        for (let route of this.routes) {
+          // data len + 1 (DEST) + 1 (EP_SSEG_ACKED) + 1 (ID)
+          let payload = new Uint8Array(this.data.length + 3)
+          payload[0] = PK.DEST
+          payload[1] = EP.SS_ACKED
+          let id = this.runningAckID
+          this.runningAckID++; this.runningAckID = this.runningAckID & 0b11111111;
+          payload[2] = id
+          payload.set(this.data, 3)
+          let datagram = PK.writeDatagram(route, payload)
+          this.acksAwaiting.push({
+            id: id,
+          })
+          this.handle(datagram, VT.STACK_ORIGIN)
+        }
+        // end conditions: we return a promise, rejecting on a timeout, resolving when all acks come back, 
+        return new Promise((resolve, reject) => {
+          let timeout = setTimeout(() => {
+            reject(`write to ${this.name} times out w/ ${this.acksAwaiting.length} acks still awaiting`)
+          }, this.timeoutLength)
+          this.acksResolve = () => {
+            clearTimeout(timeout)
+            this.acksResolve = null
+            resolve()
+          }
+          // erp, 
+          if(this.routes.length == 0) resolve() 
+        })
+      } else {
+        throw new Error(`endpoint ${this.name} written to w/ bad mode argument ${mode}, should be "acked" or "ackless"`)
+      }
+    } catch (err) {
+      throw err
+    }
+  } // end write 
+
+} // end endpoint 
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/highLevel.js b/system/javascript/osapjs/core/highLevel.js
new file mode 100644
index 0000000000000000000000000000000000000000..df3ee61b4113db5d3759c4ee2e3c118d7eeaf576
--- /dev/null
+++ b/system/javascript/osapjs/core/highLevel.js
@@ -0,0 +1,238 @@
+/*
+highLevel.js
+
+osap high level prototypes / notions and configs 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import PK from './packets.js'
+import TIME from './time.js'
+import { VT, EP } from './ts.js'
+
+export default function HighLevel(osap) {
+  // ------------------------------------------------------ KeepAlive Codes 
+  let runKA = async (vvt, freshness) => {
+    try {
+      // random stutter to avoid single-step overcrowding in large systems 
+      // console.warn(`setting up keepAlive for ${vvt.name} at ${freshness}ms interval`)
+      await TIME.delay(Math.random() * freshness)
+      let lastErrorCount = 0
+      let lastDebugCount = 0
+      // looping KA 
+      let first = false
+      while (true) {
+        let stat = await osap.mvc.getContextDebug(vvt.route)
+        if (first) {
+          lastErrorCount = stat.errorCount
+          lastDebugCount = stat.debugCount
+          first = false
+        } else {
+          if (stat.errorCount > lastErrorCount) {
+            lastErrorCount = stat.errorCount
+            stat = await osap.mvc.getContextDebug(vvt.route, "error")
+            console.error(`ERR from ${vvt.name}: ${stat.msg}`)
+          }
+          if (stat.debugCount > lastDebugCount) {
+            lastDebugCount = stat.debugCount
+            stat = await osap.mvc.getContextDebug(vvt.route, "debug")
+            console.warn(`LOG from ${vvt.name}: ${stat.msg}`)
+          }
+        }
+        await TIME.delay(freshness)
+      }
+    } catch (err) {
+      console.error(`KA: keepAlive on ${vvt.name} ends w/ failure:`, err)
+    }
+  }
+
+  // ... 
+  this.addToKeepAlive = async (name, freshness = 1000) => {
+    try {
+      // find it, start it... 
+      let list = await osap.nr.findMultiple(name)
+      if (list.length == 0) throw new Error(`can't find any instances of '${name}' in this graph...`)
+      console.log(`KA: adding ${list.length}x '${name}' to the keepAlive loop`)
+      for (let vvt of list) {
+        runKA(vvt, freshness)
+      }
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Bus Broadcast Routes 
+  this.buildBroadcastRoute = async (transmitterName, targetNames, recipientName, log = false, graph) => {
+    if (!Array.isArray(targetNames)) throw new Error(`BBR: needs a list of target names, not singletons`)
+    try {
+      // ---------------------------------------- 1. get a local image of the graph, also configured routes 
+      if(!graph) graph = await osap.nr.sweep()
+      if (log) console.log(`BBR: got a graph image...`)
+      await osap.mvc.fillRouteData(graph)
+      if (log) console.log(`BBR: reclaimed route data for the graph...`)
+      // ---------------------------------------- 1: get vvts for the transmitter, and each recipient... 
+      let transmitter = await osap.nr.find(transmitterName, graph)
+      if (log) console.log(`BBR: found 1x transmitter endpoint '${transmitter.name}', now collecting recipients...`)
+      let potentials = await osap.nr.findMultiple(recipientName, graph)
+      // we only want those recipients that are within our named firmware, so 
+      let recipients = []
+      for (let rx of potentials) {
+        for (let name of targetNames) {
+          if (rx.parent.name == name) {
+            recipients.push({ endpoint: rx })
+            break
+          }
+        }
+      }
+      // we probably want one endpoint per target-device supplied, so 
+      if(recipients.length > targetNames.length) throw new Error(`BBR: likely that we missed an endpoint...`)
+      if (log) console.log(`BBR: found ${recipients.length}x recipient endpoints '${recipientName}'`)
+      // ---------------------------------------- 2: poke around amongst each recipient to find bus drops, 
+      recipientLoop: for (let rx of recipients) {
+        for (let sib of rx.endpoint.parent.children) {
+          if (sib.type == VT.VBUS) {
+            rx.vbus = sib
+            continue recipientLoop;
+          }
+        }
+      }
+      // we now have a list of recipients: [{endpoint: <>, vbus: <>}]
+      // ---------------------------------------- 3: for each pair, find channels that already work, and first-free channel
+      for (let rx of recipients) {
+        // find first-free 
+        for (let ch in rx.vbus.broadcasts) {
+          if (rx.vbus.broadcasts[ch] == undefined) {
+            rx.firstFree = ch
+            break
+          }
+        }
+        // find existing, 
+        rx.existingChannel = null
+        for (let ch in rx.vbus.broadcasts) {
+          let channel = rx.vbus.broadcasts[ch]
+          if (channel != undefined) {
+            let walk = osap.nr.routeWalk(channel, rx.vbus)
+            if (walk.state == 'incomplete') throw new Error(`previously config'd bus channel looks broken?`)
+            if (walk.path[walk.path.length - 1] == rx.endpoint) {
+              if (log) console.log(`BBR: found an existing drop-route on ch ${ch}`)
+              rx.existingChannel = ch
+              break;
+            }
+          }
+        }
+      }
+      // console.log(`recipients`, recipients)
+      // ---------------------------------------- 4: if all recipients are on the same existing channel, that's us, 
+      let existingChannel = recipients[0].existingChannel
+      if (existingChannel) {
+        for (let rx of recipients) {
+          if (rx.existingChannel == existingChannel) {
+            // great, carry on... 
+          } else {
+            throw new Error(`BBR: looks like *some* drops have an existing ch, not all, awkward diff...`)
+          }
+        }
+      }
+      // ---------------------------------------- 5: pick a channel to build, and build drop-side, 
+      let channelSelect = 0
+      if (!existingChannel) {
+        // select the first available, 
+        for (let rx of recipients) {
+          if (parseInt(rx.firstFree) > channelSelect) channelSelect = parseInt(rx.firstFree)
+        }
+        // build drop-routes... 
+        for (let rx of recipients) {
+          let routeFromVBus = osap.nr.findRoute(rx.vbus, rx.endpoint)
+          if (!routeFromVBus) throw new Error(`failed to find vbus-to-endpoint route...`)
+          await osap.mvc.setVBusBroadcastChannel(rx.vbus.route, channelSelect, routeFromVBus)
+          if (log) console.log(`BBR: just built one new drop-route on ch ${channelSelect} at ${rx.endpoint.parent.name}`)
+          if (log) PK.logRoute(routeFromVBus, false)
+        }
+      } else {
+        channelSelect = existingChannel
+        if (log) console.log(`BBR: will use existing drop-routes on ch ${channelSelect}`)
+      }
+      // ---------------------------------------- 6: build an outgoing route to that channel... 
+      // the target is going to be...
+      let headVBus = recipients[0].vbus.reciprocals[0]
+      if (!headVBus) throw new Error(`BBR can't find the head of this vbus... ??`)
+      if (log) console.log(`BBR: found the broadcasting head within ${headVBus.parent.name}`)
+      // we want a route to this object, 
+      let routeFromTransmitter = osap.nr.findRoute(transmitter, headVBus)
+      if (!routeFromTransmitter) throw new Error(`BBR failed to walk a route from bus-broadcast transmitter to bus head`)
+      // to broadcast at the end of this, we append the broadcast instruction... 
+      routeFromTransmitter = PK.route(routeFromTransmitter).bbrd(channelSelect).end()
+      // PK.logRoute(routeFromTransmitter)
+      // let's check that the transmitter doesn't already have this route attached?
+      let prevTxRoute = false
+      for (let rt of transmitter.routes) {
+        if (PK.routeMatch(rt, routeFromTransmitter)) {
+          if (log) console.log(`BBR: a route from the transmitter to the head-vbus already exists`)
+          if (log) PK.logRoute(routeFromTransmitter, false)
+          prevTxRoute = true
+          break
+        }
+      }
+      // can set that up as well...
+      if (!prevTxRoute) {
+        // no acks on broadcasts, 
+        routeFromTransmitter.mode = EP.ROUTEMODE_ACKLESS 
+        await osap.mvc.setEndpointRoute(transmitter.route, routeFromTransmitter)
+        if (log) console.log(`BBR: we've just built a new route from the transmitter to the bus head`)
+        if (log) PK.logRoute(routeFromTransmitter, false)
+      }
+      // now return something that we could use later to delete 'em with?
+      // or just go back to stateless / name-finding... 
+      return channelSelect
+    } catch (err) {
+      console.error(`failed to build broadcast route from ${transmitterName} to '${recipientName}'s`)
+      throw err
+    }
+  } // end buildBroadcastRoute 
+
+  this.removeBroadcastRoute = async (channel, log = false) => {
+    channel = parseInt(channel)
+    try {
+      // ---------------------------------------- 1. get a local image of the graph, also configured routes 
+      let graph = await osap.nr.sweep()
+      if (log) console.log(`BBRemove: got a graph image...`)
+      await osap.mvc.fillRouteData(graph)
+      if (log) console.log(`BBRemove: reclaimed route data for the graph...`)
+      // ---------------------------------------- 2. flatten everything: we're assuming there's one (1) bus, anything on this ch is ripe for rm 
+      let list = osap.nr.flatten(graph) 
+      // ---------------------------------------- 3. look around for vbusses, check this ch, delete, 
+      for(let vvt of list){
+        if(vvt.type == VT.VBUS){
+          if(vvt.broadcasts[channel]){
+            if(log) console.log(`BBRemove: rming channel from ${vvt.parent.name}'s ${vvt.name}`)
+            await osap.mvc.removeVBusBroadcastChannel(vvt.route, channel)
+          }
+        }
+      }
+      // ---------------------------------------- 4. rm anything transmitting on this channel... 
+      for(let vvt of list){
+        if(vvt.type == VT.ENDPOINT){
+          for(let rt in vvt.routes){
+            rt = parseInt(rt)
+            let route = vvt.routes[rt]
+            if(route.path[route.path.length - 2] == PK.BBRD){
+              if(route.path[route.path.length - 1] == channel){
+                if(log) console.log(`BBRemove: rming broadcast route from ${vvt.parent.name}'s ${vvt.name}`)
+                await osap.mvc.removeEndpointRoute(vvt.route, rt)
+                return 
+              }
+            }
+          }
+        }
+      }
+    } catch (err) {
+      throw err
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/loop.js b/system/javascript/osapjs/core/loop.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c1363dc55cb5622db3d0898e6b241bebca3fca5
--- /dev/null
+++ b/system/javascript/osapjs/core/loop.js
@@ -0,0 +1,226 @@
+/*
+loop.js
+
+osap / runtime 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { VT } from "./ts.js"
+import TIME from "./time.js"
+import PK from './packets.js'
+
+let LOGHANDLER = false
+let LOGSWITCH = false
+// sed default arg to log-or-not, or override at specific call... 
+let LOGLOOP = (msg, pck = null, log = false) => {
+  if (log) console.warn('LP: ' + msg)
+  if (log && pck) PK.logPacket(pck)
+}
+
+let loopItems = []
+
+// this should be called just once per cycle, from the root vertex, 
+let osapLoop = (root) => {
+  // time is now, 
+  let now = TIME.getTimeStamp()
+  // reset our list of items-to-handle, 
+  loopItems = []
+  // collect 'em recursively, 
+  collectRecursor(root)
+  // we want to pre-compute each items' time until death, this is handy in two places, 
+  for (let item of loopItems) {
+    item.timeToDeath = item.timeToLive - (now - item.arrivalTime)
+  }
+  // sort items by their time-to-live,
+  loopItems.sort((a, b) => {
+    // for the compare function, we return `> 0` if we want to sort a after b,
+    // so we just want a's ttd - b's ttd, items which have more time until failure will 
+    // be serviced *after* items whose life is on the line etc 
+    return a.timeToDeath - b.timeToDeath
+  })
+  //console.warn(loopItems.length)
+  // now we just go through each item, in order, and try to handle it...   
+  for (let i = 0; i < loopItems.length; i++) {
+    // handle 'em ! 
+    osapItemHandler(loopItems[i])
+  }
+  // console.log(`handled ${loopItems.length}`)
+  // that's the end of the loop, folks 
+  // items which have gone unhandled will have issued requests for new loops, 
+  // this will fire again on those cycles, 
+}
+
+let collectRecursor = (vt) => {
+  // we want to collect items from input & output stacks alike, 
+  for (let od = 0; od < 2; od++) {
+    for (let i = 0; i < vt.stack[od].length; i++) {
+      loopItems.push(vt.stack[od][i])
+    }
+  }
+  // then collect our children's items...
+  for (let child of vt.children) {
+    collectRecursor(child)
+  }
+}
+
+let osapItemHandler = (item) => {
+  LOGLOOP(`handling at ${item.vt.name}`, item.data)
+  // kill deadies 
+  if (item.timeToDeath < 0) {
+    LOGLOOP(`LP: item at ${item.vt.name} times out`, null, true)
+    item.handled(); return
+  }
+  // find ptrs, 
+  let ptr = PK.findPtr(item.data)
+  if (ptr == undefined) {
+    LOGLOOP(`item at ${item.vt.name} is ptr-less`, item.data, true)
+    item.handled(); return
+  }
+  // now we can try to transport it, switching on the instruction (which is ahead)
+  switch (PK.readKey(item.data, ptr + 1)) {
+    // packet is at destination, send to vertex to handle, 
+    // if handler returns true, OK to destroy packet, else wait on it 
+    case PK.DEST:
+      item.vt.destHandler(item, ptr)
+      break;
+    // reply to pings
+    case PK.PINGREQ:
+      item.vt.pingRequestHandler(item, ptr)
+      break;
+    // handle replies *from* pings, 
+    case PK.PINGRES:
+      item.vt.pingResponseHandler(item, ptr)
+      break;
+    // reply to scopes
+    case PK.SCOPEREQ:
+      item.vt.scopeRequestHandler(item, ptr)
+      break;
+    // handle replies *from* scopes 
+    case PK.SCOPERES:
+      item.vt.scopeResponseHandler(item, ptr)
+      break;
+    // do internal transport, 
+    case PK.SIB:
+    case PK.PARENT:
+    case PK.CHILD:
+      osapInternalTransport(item, ptr)
+      break;
+    // do port-forwarding transport, 
+    case PK.PFWD:
+      // only possible if vertex is a vport, 
+      if (item.vt.type == VT.VPORT) {
+        // and if it's clear to send, 
+        if (item.vt.cts()) {
+          LOGLOOP(`pfwd OK at ${item.vt.name}`)
+          // walk the ptr 
+          PK.walkPtr(item.data, ptr, item.vt, 1)
+          // send it... if we were to operate total-packet-ttl, we would also  
+          // decriment the packet's ttl counter, but at the time of writing (2022-06-17) 
+          // we are operating on per-hop ttl, 
+          item.vt.send(item.data)
+          item.handled()
+        } else {
+          LOGLOOP(`pfwd hold, not CTS at ${item.vt.name}`);
+          item.vt.requestLoopCycle()
+        }
+      } else {
+        LOGLOOP(`pfwd at non-vport, ${item.vt.name} is type ${item.vt.type}`, item.data, true)
+        item.handled()
+      }
+      break;
+    case PK.BFWD:
+    case PK.BBRD:
+      LOGLOOP(`bus transport request in JS, at ${item.vt.name}`, item.data, true)
+      break;
+    case PK.LLESCAPE:
+      LOGLOOP(`low level escape msg from ${item.vt.name}`, null, true)
+      break;
+    default:
+      LOGLOOP(`LP: item at ${item.vt.name} has unknown packet key after ptr, bailing`, item.data, true)
+      item.handled()
+      break;
+  } // end item switch, 
+}
+
+// here we want to look thru potentially multi-hop internal moves & operate that transport... 
+// i.e. we want to tunnel straight thru multiple steps, using the DAG as an addressing space 
+// but not necessarily transport space 
+let osapInternalTransport = (item, ptr) => {
+  try {
+    // starting at the items' vertice... 
+    let vt = item.vt
+    // new ptr to walk fwds, 
+    let fwdPtr = ptr + 1
+    // count # of ops, 
+    let opCount = 0
+    // loop thru internal ops until we hit a destination of a forwarding step, 
+    fwdSweep: for (let h = 0; h < 16; h++) {
+      LOGLOOP(`fwd look from ${vt.name}, ptr ${fwdPtr} key ${item.data[fwdPtr]}`)
+      switch (PK.readKey(item.data, fwdPtr)) {
+        // these are the internal transport cases: across, up, or down the tree 
+        case PK.SIB:
+          LOGLOOP(`instruction is sib, ${PK.readArg(item.data, fwdPtr)}`)
+          if (!vt.parent) { throw new Error(`fwd to sib from ${vt.name}, but no parent exists`) }
+          let sib = vt.parent.children[PK.readArg(item.data, fwdPtr)]
+          if (!sib) { throw new Error(`fwd to sib ${PK.readArg(item.data, fwdPtr)} from ${vt.name}, but none exists`) }
+          vt = sib
+          break;
+        case PK.PARENT:
+          LOGLOOP(`instruction is parent, ${PK.readArg(item.data, fwdPtr)}`)
+          if (!vt.parent) { throw new Error(`fwd to parent from ${vt.name}, but no parent exists`) }
+          vt = vt.parent
+          break;
+        case PK.CHILD:
+          LOGLOOP(`instruction is child, ${PK.readArg(item.data, fwdPtr)}`)
+          let child = vt.children[PK.readArg(item.data, fwdPtr)]
+          if (!child) { throw new Error(`fwd to child ${PK.readArg(item.data, fwdPtr)} from ${vt.name}, none exists`) }
+          vt = child
+          break;
+        // these are all cases where i.e. the vt itself will handle, or networking will happen, 
+        case PK.PFWD:
+        case PK.BFWD:
+        case PK.BBRD:
+        case PK.DEST:
+        case PK.PINGREQ:
+        case PK.PINGRES:
+        case PK.SCOPEREQ:
+        case PK.SCOPERES:
+        case PK.LLESCAPE:
+          LOGLOOP(`context exit at ${vt.name}, counts ${opCount} ops`)
+          // this is the end stop, we should see if we can transport in, 
+          if (vt.stackAvailableSpace(VT.STACK_DEST) >= 0) {
+            LOGLOOP(`clear to shift in to ${vt.name} from ${item.vt.name}, shifting...`)
+            // we shift ptrs up, 
+            PK.walkPtr(item.data, ptr, item.vt, opCount)
+            // and ingest it at the new place, clearing the source, 
+            vt.handle(item.data, VT.STACK_DEST)
+            item.handled()
+          } else {
+            LOGLOOP(`flow-controlled from ${vt.name} to ${item.vt.name}, awaiting...`)
+            item.vt.requestLoopCycle()
+          }
+          // fwd-look is terminal here in all cases, 
+          break fwdSweep;
+        default:
+          LOGLOOP(`internal transport failure, bad key ${item.data[fwdPtr]}`)
+          item.handled()
+          return
+      } // end switch 
+      fwdPtr += 2;
+      opCount++;
+    }
+  } catch (err) {
+    console.error(err)
+    item.handled()
+    return 
+  }
+}
+
+export { osapLoop }
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/mvc.js b/system/javascript/osapjs/core/mvc.js
new file mode 100644
index 0000000000000000000000000000000000000000..73399316ac49ffbd9621a2655bf2fcfad34f66fa
--- /dev/null
+++ b/system/javascript/osapjs/core/mvc.js
@@ -0,0 +1,476 @@
+/*
+osapMVC.js
+
+getters and setters, etc, for remote elements 
+
+so far, almost entirely to-do with route config,
+in the future, will be used more broadly: we probably want a stronger underlying type system 
+and gettings / setters that generalize on those... i.e. structs : remote structs etc, 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS, VT, EP, VBUS } from './ts.js'
+import TIME from './time.js'
+import PK from './packets.js'
+
+let ROUTEREQ_MAX_TIME = 2000 // ms 
+
+let RT = {
+  DBG_STAT: 151,
+  DBG_ERRMSG: 152,
+  DBG_DBGMSG: 153,
+  DBG_RES: 161,
+}
+
+export default function OMVC(osap) {
+  // ------------------------------------------------------ Query IDs
+  // msgs all have an ID... 
+  // we just use one string of 'em, then can easily dispatch callbacks, 
+  let runningQueryID = 112
+  let getNewQueryID = () => {
+    runningQueryID++
+    runningQueryID = runningQueryID & 0b11111111
+    return runningQueryID
+  }
+  let queriesAwaiting = []
+
+  // ------------------------------------------------------ Context Debuggen 
+  this.getContextDebug = async (route, stream = "none", maxRetries = 3) => {
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+      let id = getNewQueryID()
+      // these are all going to get more or less the same response, 
+      let payload = new Uint8Array([PK.DEST, 0, id])
+      switch (stream) {
+        case "none":
+          payload[1] = RT.DBG_STAT
+          break;
+        case "error":
+          payload[1] = RT.DBG_ERRMSG
+          break;
+        case "debug":
+          payload[1] = RT.DBG_DBGMSG
+          break;
+        default:
+          throw new Error("odd stream spec'd for getContextDebug, should be 'error' or 'debug'")
+      } // end switch 
+      let datagram = PK.writeDatagram(route, payload)
+      osap.handle(datagram, VT.STACK_ORIGIN)
+      // handler
+      return new Promise((resolve, reject) => {
+        let retries = 0
+        let timeoutFn = () => {
+          if (retries > maxRetries) {
+            reject(`debug collect timeout to ${route.path}`)
+          } else {
+            retries++
+            console.warn(`CONTEXT DEBUG RETRYING... count ${retries}`)
+            osap.handle(datagram, VT.STACK_ORIGIN)
+            timeout = setTimeout(timeoutFn, 1000)
+          }
+        }
+        let timeout = null
+        queriesAwaiting.push({
+          id: id,
+          onResponse: function (data) {
+            clearTimeout(timeout)
+            let res = {
+              loopHighWaterMark: TS.read("uint32", data, 0),
+              errorCount: TS.read("uint32", data, 4),
+              debugCount: TS.read("uint32", data, 8)
+            }
+            if (stream != "none") {
+              res.msg = TS.read("string", data, 12).value
+            }
+            resolve(res)
+          }
+        })
+        timeout = setTimeout(timeoutFn, 1000)
+      })
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Batch Route Infill 
+
+  this.fillRouteData = async (graph) => {
+    try {
+      // we'll make lists of endpoints & vbussess, 
+      let endpoints = []
+      let busses = []
+      let vertices = osap.nr.flatten(graph)
+      for (let vt of vertices) {
+        if (vt.type == VT.ENDPOINT) endpoints.push(vt)
+        if (vt.type == VT.VBUS) busses.push(vt)
+      }
+      // then just get through 'em and collect routes 
+      for (let ep of endpoints) {
+        let routes = await this.fillEndpointRoutes(ep.route)
+        ep.routes = routes
+      }
+      for (let vbus of busses) {
+        let broadcasts = await this.fillVBusBroadcastChannels(vbus.route)
+        // append broadcasts to vbus...
+        vbus.broadcasts = broadcasts
+      }
+      // we've been editing by reference, so the graph is now 'full' 
+      return graph
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Per-Endpoint Route List Collection 
+  this.fillEndpointRoutes = async (route) => {
+    // alright, do it in a loop until they return an empty array, 
+    // also... endpoint route objects, should *not* return the trailing three digits (?) 
+    // or should ? the vvt .route object doesn't, 
+    try {
+      let indice = 0, routes = []
+      while (true) {
+        let epRoute = await this.getEndpointRoute(route, indice)
+        if (epRoute != undefined) {
+          routes[indice] = epRoute
+          indice++
+        } else {
+          break
+        }
+      } // end while 
+      return routes
+    } catch (err) {
+      // pass it up... 
+      console.error(err)
+      throw (err)
+    }
+  }
+
+  // ------------------------------------------------------ Per-VBus Broadcast Collection 
+  this.fillVBusBroadcastChannels = async (route) => {
+    // bus channels are not necessarily stacked up (0-n) like broadcast channels are, 
+    // i.e they might be sparse: we can't just ask for "how many" and then query 0-n,
+    // since i.e. some previosly-configured broadcast is useful on new bus drops ...
+    // but we query 0-n, get channels at each indice, throw indice away, taking for granted
+    // that while we're querying, no one else is adding / rm'ing channel configs... 
+    // route is a route *to* the vbus, so we are making vbus mvc requests... 
+    try {
+      // this collects that map, should be an array of some fixed length, w/ 'undefined' in 
+      // empty slots, and the string literal 'exists' in channels where routes exist... 
+      // then we go through per channel and query... 
+      let map = await this.getVBusBroadcastMap(route)
+      for (let ch = 0; ch < map.length; ch++) {
+        if (map[ch] == 'exists') {
+          let channelRoute = await this.getVBusBroadcastChannel(route, ch)
+          map[ch] = channelRoute
+        }
+      }
+      return map
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Per-VBus Broadcast Map 
+  this.getVBusBroadcastMap = async (route) => {
+    // wait for clear space, 
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    } catch (err) {
+      throw err
+    }
+    // payload... 
+    let id = getNewQueryID()
+    let payload = new Uint8Array([PK.DEST, VBUS.BROADCAST_MAP_REQ, id])
+    let datagram = PK.writeDatagram(route, payload)
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // handler, 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject(`vbus broadcast map req timeout to ${route.path}`)
+        }, 1000),
+        onResponse: function (data) {
+          // clear timer, 
+          clearTimeout(this.timeout)
+          // bytes 0, 1 are length 
+          let rptr = 0
+          let map = new Array(data[rptr++])
+          let bitByteModulo = 0
+          for (let ch = 0; ch < map.length; ch++) {
+            map[ch] = (data[rptr] & (1 << bitByteModulo) ? 'exists' : undefined) // lol, 
+            bitByteModulo++
+            if (bitByteModulo >= 8) {
+              bitByteModulo = 0
+              rptr++
+            }
+          }
+          resolve(map)
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ VBus Broadcast Channel Collect
+  this.getVBusBroadcastChannel = async (route, channel) => {
+    // wait for clear space, 
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    } catch (err) {
+      throw err
+    }
+    // payload is pretty simple, 
+    let id = getNewQueryID()
+    let payload = new Uint8Array([PK.DEST, VBUS.BROADCAST_QUERY_REQ, id, channel])
+    let datagram = PK.writeDatagram(route, payload)
+    // ship it from the root vertex, 
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // setup handler, 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject(`vbus broadcast ch req timeout to ${route.path}`)
+        }, 1000),
+        onResponse: function (data) {
+          // clear timer, 
+          clearTimeout(this.timeout)
+          // make a new route object for our caller, 
+          if (data[0] == 0) {
+            // [0] here means emptiness 
+            resolve()
+          } else {
+            resolve({
+              ttl: TS.read('uint16', data, 1),
+              segSize: TS.read('uint16', data, 3),
+              path: new Uint8Array(data.subarray(5))
+            })
+          }
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ VBus Broadcast Channel Set 
+  this.setVBusBroadcastChannel = async (routeToVBus, channel, routeFromChannel) => {
+    channel = parseInt(channel)
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    } catch (err) {
+      throw err
+    }
+    let id = getNewQueryID()
+    // + DEST, + ROUTE_SET, + ID, + CH + Route (route.length + ttl + segsize)
+    let payload = new Uint8Array(4 + routeFromChannel.path.length + 4)
+    payload.set([PK.DEST, VBUS.BROADCAST_SET_REQ, id, channel])
+    let wptr = 4
+    // though broadcast channels don't use 'em yet, we just serialize as a 'route' type... 
+    wptr += TS.write('uint16', routeFromChannel.ttl, payload, wptr)
+    wptr += TS.write('uint16', routeFromChannel.segSize, payload, wptr)
+    payload.set(routeFromChannel.path, wptr)
+    // grams grams grams 
+    let datagram = PK.writeDatagram(routeToVBus, payload)
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // handler... 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject(`broadcast channel set req timeout`)
+        }, 1000),
+        onResponse: function (data) {
+          //console.warn(`ROUTE SET REPLY`)
+          if (data[0]) {
+            resolve()
+          } else {
+            reject(`badness error code ${data} from vbus, on try-to-set-new-broadcast`)
+          }
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ VBus Broadcast Channel Remove 
+  this.removeVBusBroadcastChannel = async (routeToVBus, channel) => {
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    } catch (err) {
+      throw err
+    }
+    let id = getNewQueryID()
+    let payload = new Uint8Array([PK.DEST, VBUS.BROADCAST_RM_REQ, id, channel])
+    let datagram = PK.writeDatagram(routeToVBus, payload)
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // setup handler, 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject('broadcast ch rm req timeout')
+        }, 1000),
+        onResponse: function (data) {
+          if (data[0]) {
+            resolve()
+          } else {
+            reject(`badness error code ${data[ptr + 1]} from endpoint, on try-to-delete-broadcast-channel`)
+          }
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ Per-Indice Route Collection 
+  this.getEndpointRoute = async (route, indice) => {
+    // wait for clear space, 
+    try {
+      await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    } catch (err) {
+      throw err
+    }
+    try {
+      // payload is pretty simple, 
+      let id = getNewQueryID()
+      let payload = new Uint8Array([PK.DEST, EP.ROUTE_QUERY_REQ, id, indice])
+      let datagram = PK.writeDatagram(route, payload)
+      // ship it from the root vertex, 
+      osap.handle(datagram, VT.STACK_ORIGIN)
+      return new Promise((resolve, reject) => {
+        queriesAwaiting.push({
+          id: id,
+          timeout: setTimeout(() => {
+            reject(`route req ${id} timeout to ${route.path}`)
+          }, ROUTEREQ_MAX_TIME),
+          onResponse: function (data) {
+            // make a new route object for our caller, 
+            let routeMode = data[0]
+            // if mode == 0, no route exists at this indice, resolve undefined 
+            // otherwise... resolve the route... 
+            if (routeMode == 0) {
+              resolve()
+            } else {
+              resolve({
+                mode: routeMode,
+                ttl: TS.read('uint16', data, 1),
+                segSize: TS.read('uint16', data, 3),
+                path: new Uint8Array(data.subarray(5))
+              })
+            }
+          }
+        }) // end push 
+      }) // end promise-return, 
+    } catch (err) {
+      console.error(err)
+    }
+  }
+
+  // ------------------------------------------------------ Endpoint Route-Addition Request 
+  this.setEndpointRoute = async (routeToEndpoint, routeFromEndpoint) => {
+    // not all routes have modes, set a default, 
+    if (!routeFromEndpoint.mode) { routeFromEndpoint.mode = EP.ROUTEMODE_ACKED }
+    // ok we dooooo
+    await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    // similar...
+    let id = getNewQueryID()
+    // + DEST, + ROUTE_SET, + ID, + Route (route.length + mode + ttl + segsize)
+    let payload = new Uint8Array(3 + routeFromEndpoint.path.length + 5)
+    payload.set([PK.DEST, EP.ROUTE_SET_REQ, id, routeFromEndpoint.mode])
+    let wptr = 4
+    wptr += TS.write('uint16', routeFromEndpoint.ttl, payload, wptr)
+    wptr += TS.write('uint16', routeFromEndpoint.segSize, payload, wptr)
+    payload.set(routeFromEndpoint.path, wptr)
+    // gram it up, 
+    let datagram = PK.writeDatagram(routeToEndpoint, payload)
+    // ship it 
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // setup handler 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject(`route set req timeout`)
+        }, ROUTEREQ_MAX_TIME),
+        onResponse: function (data) {
+          //console.warn(`ROUTE SET REPLY`)
+          if (data[0]) {
+            // ep's reply with the indice where they stuffed the route... 
+            resolve(data[1])
+          } else {
+            reject(`badness error code ${data} from endpoint, on try-to-set-new-route`)
+          }
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ Endpoint Route-Delete Request 
+  this.removeEndpointRoute = async (routeToEndpoint, indice) => {
+    await osap.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+    // same energy
+    let id = getNewQueryID()
+    // + DEST, + ROUTE_RM, + ID, + Indice 
+    let payload = new Uint8Array([PK.DEST, EP.ROUTE_RM_REQ, id, indice])
+    let datagram = PK.writeDatagram(routeToEndpoint, payload)
+    osap.handle(datagram, VT.STACK_ORIGIN)
+    // setup handler, 
+    return new Promise((resolve, reject) => {
+      queriesAwaiting.push({
+        id: id,
+        timeout: setTimeout(() => {
+          reject('route rm req timeout')
+        }, ROUTEREQ_MAX_TIME),
+        onResponse: function (data) {
+          if (data[0]) {
+            resolve()
+          } else {
+            reject(`badness error code ${data[ptr + 1]} from endpoint, on try-to-delete-route`)
+          }
+        }
+      })
+    })
+  }
+
+  // ------------------------------------------------------ Destination Handler: Dispatching Replies 
+  this.destHandler = (item, ptr) => {
+    // here data[ptr] == PK.PTR, then ptr + 1 is PK.DEST, ptr + 2 is key for us, 
+    // ... we could do: 
+    // mvc things w/ one attach-and-release reponse handlers and root-unique request IDs, non?
+    keySwitch: switch (item.data[ptr + 2]) {
+      case EP.ROUTE_QUERY_RES:
+      case EP.ROUTE_SET_RES:
+      case EP.ROUTE_RM_RES:
+      case VBUS.BROADCAST_MAP_RES:
+      case VBUS.BROADCAST_QUERY_RES:
+      case VBUS.BROADCAST_SET_RES:
+      case VBUS.BROADCAST_RM_RES:
+      case RT.ERR_RES:
+      case RT.DBG_RES:
+        {
+          // match to id, send to handler, carry on... 
+          let rqid = item.data[ptr + 3]
+          for (let rq in queriesAwaiting) {
+            if (queriesAwaiting[rq].id == rqid) {
+              // do onResponse w/ reply-specific payload... 
+              queriesAwaiting[rq].onResponse(new Uint8Array(item.data.subarray(ptr + 4)))
+              queriesAwaiting.splice(rq, 1)
+              break keySwitch;
+            }
+          }
+          // some network retries etc can result in double replies... this is OK, happens... 
+          console.warn(`recvd mvc response ${rqid}, but no matching req awaiting... of ${queriesAwaiting.length}`)
+          break;
+        }
+      default:
+        console.error(`unrecognized key in osap root / mvc dest handler, ${item.data[ptr]}`)
+        PK.logPacket(item.data)
+    } // end switch, 
+    // all mvc replies get *handled* 
+    item.handled()
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/netRunner.js b/system/javascript/osapjs/core/netRunner.js
new file mode 100644
index 0000000000000000000000000000000000000000..75ab93ef2c741f7e4dff3bf4a384480d20498086
--- /dev/null
+++ b/system/javascript/osapjs/core/netRunner.js
@@ -0,0 +1,379 @@
+/*
+netRunner.js
+
+graph search routines, in JS, for OSAP systems 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS, VT } from './ts.js'
+import TIME from './time.js'
+import PK from './packets.js'
+
+export default function NetRunner(osap) {
+
+  let scanStartTime = 0
+  let allNetworkVertices = []
+  let latestSweep = {
+    runTime: 0,
+    graph: null
+  }
+
+  // runs a sweep, starting at the osap root vertex 
+  this.sweep = async () => {
+    scanStartTime = TIME.getTimeStamp()
+    // to find loops, we keep a list of all of the network-capable vertices (vports and vbusses) that we find,
+    allNetworkVertices = []
+    return new Promise(async (resolve, reject) => {
+      try {
+        // make root from our own root note... 
+        let root = await osap.scope(PK.route().end(250, 128), scanStartTime)
+        // try a recursor based on root objects, using also our entry point, 
+        await this.searchContext(root, null)
+        console.warn(`SWEEP done in ${TIME.getTimeStamp() - scanStartTime}ms`)
+        latestSweep = {
+          runTime: TIME.getTimeStamp(),
+          graph: root
+        }
+        resolve(root)
+      } catch (err) {
+        console.error(err)
+        reject('sweep fails')
+      }
+    })
+  }
+
+  this.getLatestSweep = async (freshness = 750) => {
+    try {
+      if (TIME.getTimeStamp() - latestSweep.runTime > freshness) {
+        console.warn(`Refreshing sweep...`)
+        let graph = await this.sweep()
+        latestSweep = {
+          runTime: TIME.getTimeStamp(),
+          graph: graph
+        }
+      }
+      return latestSweep.graph
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.forceSweepUpdate = async () => {
+    try {
+      let graph = await this.sweep()
+      latestSweep = {
+        runTime: TIME.getTimeStamp(),
+        graph: graph
+      }
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.searchContext = async (root, entrance) => {
+    try {
+      let contextScanTime = TIME.getTimeStamp()
+      // get data for new children... i.e. the entrance should already be all hooked up, 
+      for (let c = 0; c < root.children.length; c++) {
+        if (root.children[c] == undefined) {
+          root.children[c] = await osap.scope(PK.route(root.route).child(c).end(), contextScanTime)
+          root.children[c].parent = root
+        }
+      }
+      // stash flat-list of all network-capable vertices, for loop detection 
+      for (let c = 0; c < root.children.length; c++) {
+        allNetworkVertices.push(root.children[c])
+      }
+      // now poke through, check on network hops, 
+      for (let c = 0; c < root.children.length; c++) {
+        let vt = root.children[c]
+        // don't traverse back from whence we came, 
+        if (vt == entrance) continue
+        if (vt.type == VT.VPORT) { //---------------------------------- if vport, find vport partner, 
+          if (vt.linkState) {
+            // we try to catch the reciprocal, or we time out... 
+            let reciprocal = {}
+            try {
+              // get reciprocal port & reverse-plumb, 
+              reciprocal = await osap.scope(PK.route(root.route).child(c).pfwd().end(), contextScanTime)
+              reciprocal.reciprocal = vt
+              // loop detect... just fail, we did handle these before, 
+              if (loopDetect(reciprocal)) continue;
+              // and that things' parent, 
+              reciprocal.parent = await osap.scope(PK.route(reciprocal.route).parent().end(), contextScanTime)
+              // if that works, we do a little plumbing:
+              reciprocal.parent.children[reciprocal.indice] = reciprocal
+              // then we can carry on to the next, 
+              await this.searchContext(reciprocal.parent, reciprocal)
+            } catch (err) {
+              console.warn(`${vt.name} at ${root.name}'s reciprocal traverse error, reason:`, err)
+              reciprocal = { type: "unreachable" }
+            }
+            // plumb it & reverse it, 
+            vt.reciprocal = reciprocal
+          } else {
+            vt.reciprocal = { type: "unreachable" }
+          }
+        } else if (vt.type == VT.VBUS) { //--------------------------- if vbus, find bus partner for each... 
+          allNetworkVertices.push(vt)
+          for (let d = 0; d < vt.linkState.length; d++) {
+            let reciprocal = {}
+            if (vt.linkState[d]) {
+              try {
+                reciprocal = await osap.scope(PK.route(root.route).child(c).bfwd(d).end(), contextScanTime)
+                reciprocal.reciprocals[vt.ownRxAddr] = vt
+                if (loopDetect(reciprocal)) continue;
+                reciprocal.parent = await osap.scope(PK.route(reciprocal.route).parent().end(), contextScanTime)
+                reciprocal.parent.children[reciprocal.indice] = reciprocal
+                await this.searchContext(reciprocal.parent, reciprocal)
+              } catch (err) {
+                console.warn(`${vt.name}'s reciprocal traverse error, reason:`, err)
+                reciprocal = { type: "unreachable" }
+              }
+            } else {
+              reciprocal = { type: "unreachable" }
+            }
+            // plumb it, & the reverse... 
+            vt.reciprocals[d] = reciprocal
+          }
+        }
+      }
+    } catch (err) {
+      console.warn(`Search at ${root.name} fails`)
+      console.error(err)
+    }
+  }
+
+  let loopDetect = (nv) => {
+    return false
+    // if this has been tagged previously at some time *since* we started the most recent scan, 
+    if (nv.previousTimeTag > scanStartTime) {
+      // it's likely a duplicate / loop of something we've already scanned, so go looking:
+      for (let v of allNetworkVertices) {
+        if (v.type == "unreachable") continue;
+        if (v.name == nv.name) {
+          throw new Error('this is a candidate, but this fn is unfinished: need time info to pick uniqueness')
+        }
+      }
+      throw new Error('loop detecting fn is unfinished: plan was to detect & plumb loops here, returning true')
+    } else {
+      return false
+    }
+  }
+
+  this.flatten = (graph) => {
+    let list = []
+    let recursor = (root, entrance) => {
+      //console.log(`recursing ${root.name}`)
+      list.push(root)
+      for (let child of root.children) {
+        list.push(child)
+        if (child == entrance) continue
+        if (child.type == VT.VPORT) {
+          if (child.reciprocal.type != "unreachable") {
+            recursor(child.reciprocal.parent, child.reciprocal)
+          }
+        } else if (child.type == VT.VBUS) {
+          for (let recip of child.reciprocals) {
+            if (recip.type != "unreachable") {
+              recursor(recip.parent, recip)
+            }
+          }
+        }
+      } // end for-child-of-root,
+    } // end recursor
+    recursor(graph)
+    return list
+  }
+
+  // also not loop-safe atm, 
+  this.find = async (vtName, start) => {
+    try {
+      let list = await this.findMultiple(vtName, start)
+      if (list.length > 1) throw new Error(`found *multiple* instances of '${vtName}' !`)
+      if (list.length == 0) throw new Error(`can't find any vertex w/ the name '${vtName}' in this graph`)
+      return list[0]
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.findMultiple = async (vtName, start) => {
+    try {
+      let list = []
+      if (!start) start = await this.getLatestSweep()
+      let candidates = this.flatten(start)
+      for (let vvt of candidates) {
+        // if(vvt.name.includes("rt_")) console.log(vvt.name)
+        if (vvt.name == vtName) list.push(vvt)
+      }
+      return list
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.findWithin = async (vtName, parentName, start) => {
+    try {
+      let list = []
+      if (!start) start = await this.getLatestSweep()
+      let candidates = this.flatten(start)
+      for (let vvt of candidates) {
+        if (vvt.name == parentName) {
+          for (let child of vvt.children) {
+            if (child.name == vtName) return child
+          }
+        }
+      }
+      throw new Error(`no vertex ${vtName} found within any ${parentName}`)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // mmmm, this would be in "highLevel" and could include options to find-within for each... 
+  this.connect = async (headName, tailName) => {
+    try {
+      let graph = await this.sweep()
+      console.warn(graph)
+      let head = await this.find(headName, graph)
+      let tail = await this.find(tailName, graph)
+      //console.warn(`found the head, the tail...`, head, tail)
+      let route = this.findRoute(head, tail)
+      //console.warn(`the route betwixt...`, route)
+      //PK.logRoute(route)
+      // then we could do this to add the route / make the connection: 
+      await osap.mvc.setEndpointRoute(head.route, route)
+      return route
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // walks routes along a virtual graph, returning a list of stops, 
+  this.routeWalk = (route, source) => {
+    //console.log('walking', route, 'from', source)
+    if (!(route.path instanceof Uint8Array)) { throw new Error(`strange route structure in the routeWalk fn, netRunner`) }
+    // we'll make a list... of vvts, which are on this path, 
+    // walking along the route... 
+    let ptr = 1, indice = 0, vvt = source, list = []
+    // append vvt to head of list, or no? yah, should do 
+    list.push(vvt)
+    for (let s = 0; s < 16; s++) {
+      if (ptr >= route.path.length) {
+        return { path: list, state: 'complete' }
+      }
+      switch (route.path[ptr]) {
+        case PK.SIB:
+          indice = PK.readArg(route.path, ptr)
+          // do we have it ? 
+          if (vvt.parent && vvt.parent.children[indice]) {
+            vvt = vvt.parent.children[indice]
+            list.push(vvt)
+            break;
+          } else {
+            return { path: list, state: 'incomplete', reason: 'missing sib' }
+          }
+        case PK.PFWD:
+          if (vvt.reciprocal && vvt.reciprocal.type != "unreachable") {
+            vvt = vvt.reciprocal
+            list.push(vvt)
+            break
+          } else {
+            return { path: list, state: 'incomplete', reason: 'nonconn vport' }
+          }
+        default:
+          return { path: list, state: 'incomplete', reason: 'default switch' }
+      }
+      // increment to next, 
+      ptr += 2
+    }
+  }
+
+  // tool to find routes *between* two obj... head & tail should be vvts in the same graph, we want to search betwixt, 
+  // routes are returned as route objects, and are not added in this fn 
+  this.findRoute = (head, tail, log = false) => {
+    if (log) console.warn(`FR: searching from ${head.name} to ${tail.name}`, head.route, tail.route)
+    // we're going to poke around recursively, but in the cases where we ever exit *up* a bus-drop, 
+    // we need to not traverse back *down* that thing... this is that state, and it's a lazy soln', 
+    // and if we ever have more than one bus in a net we'll be in trouble w/ this 
+    let busDropLatch = 0
+    // we... recursively poke around? this is maybe le-difficult, 
+    let recursor = (route, from) => {
+      if (log) console.warn(`FR: recurse from ${from.name}, logging route:`)
+      if (log) PK.logRoute(route)
+      // copy...
+      route = {
+        ttl: route.ttl,
+        segSize: route.segSize,
+        path: new Uint8Array(route.path)
+      }
+      // how big ? 
+      if (route.path.length > 128) {
+        throw new Error(`FR: likely excess recursion here... blocking`)
+      }
+      // first... look thru siblings at this level, 
+      for (let s in from.parent.children) {
+        s = parseInt(s)
+        let sib = from.parent.children[s]
+        if (false) console.warn(`FR: eval ${sib.name}, ${tail.name}`)
+        // if that's the ticket, ship it, 
+        if (PK.routeMatch(sib.route, tail.route)) {
+          if (log) console.warn(`FR: found the target !`)
+          return PK.route(route).sib(s).end()
+        }
+      }
+      // if not, find ports, 
+      let results = []
+      for (let s in from.parent.children) {
+        s = parseInt(s)
+        let sib = from.parent.children[s]
+        if (sib.type == VT.VPORT && sib != from) {
+          if (sib.reciprocal && sib.reciprocal.type != "unreachable") {
+            // we push potential results into this collection of results & then pass them back up,
+            // there's likely a better way to cancel the recursing once we find a match 
+            // as a warning... this is not loop safe ! 
+            results.push(recursor(PK.route(route).sib(s).pfwd().end(), sib.reciprocal))
+          }
+        } else if (sib.type == VT.VBUS) {
+          if (sib.ownRxAddr == 0) { // -------------------- it's the bus head  
+            for (let d in sib.reciprocals) {
+              // don't go back down to from whence we came, 
+              if (parseInt(d) == busDropLatch) continue
+              // traverse down all other reachable drops... 
+              if (sib.reciprocals[d].type != "unreachable") {
+                results.push(recursor(PK.route(route).sib(s).bfwd(parseInt(d)).end(), sib.reciprocals[d]))
+              }
+            }
+          } else { // ------------------------------------- it's a bus drop, 
+            // only come back up if we haven't already come *up* from a drop once before 
+            if (busDropLatch) {
+              if (log) console.warn(`FR: latch prevented up-bus traversal`)
+              continue
+            }
+            // otherwise, we want to pop up to the head, like 
+            if (log) console.warn(`FR: popping up to the bus-head from '${sib.parent.name} / ${sib.name}' at bus addr ${sib.ownRxAddr}`)
+            busDropLatch = sib.ownRxAddr
+            results.push(recursor(PK.route(route).sib(s).bfwd(0).end(), sib.reciprocals[0]))
+          }
+        }
+      }
+      // done children-sweep, now look for matches, should only be one... 
+      for (let res of results) {
+        if (res != null) return res
+      }
+      // these are dead-ends in the recursion 
+      // console.log('returning null...')
+      return null
+    } // end recursor 
+    // start recursor w/ initial route-to-self, 
+    return recursor(PK.route().end(), head)
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/osap.js b/system/javascript/osapjs/core/osap.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9517fa92c3ad6046277ed5388329b034895ef2d
--- /dev/null
+++ b/system/javascript/osapjs/core/osap.js
@@ -0,0 +1,112 @@
+/*
+osap.js
+
+trunk osap object in context tree 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import { VT } from './ts.js'
+import VPort from './vport.js'
+import Vertex from './vertex.js'
+import Endpoint from './endpoint.js'
+import Query from './query.js'
+import QueryMSeg from './queryMSeg.js'
+import { osapLoop } from './loop.js'
+import NetRunner from './netRunner.js'
+import OMVC from './mvc.js'
+import HighLevel from './highLevel.js'
+
+// root is also a vertex, yah 
+export default class OSAP extends Vertex {
+  // yes, but !parent, indice == 0 
+  constructor(name = "unnamed") {
+    super(null, 0)
+    this.name = "rt_" +name 
+  }
+
+  // ... 
+  type = VT.ROOT
+
+  // children factories 
+  vPort = (name) => {
+    let np = new VPort(this, this.children.length)
+    if (name) np.name = "vp_" + name
+    this.children.push(np)
+    return np
+  }
+  module = (name) => {
+    let md = new Module(this, this.children.length)
+    if (name) md.name = "md_" + name
+    this.children.push(md)
+    return md
+  }
+  endpoint = (name) => {
+    let ep = new Endpoint(this, this.children.length)
+    if (name) ep.name = "ep_" + name
+    this.children.push(ep)
+    return ep
+  }
+  query = (route, retries = 2) => {
+    let qr = new Query(this, this.children.length, route, retries)
+    qr.name = `qr_${this.children.length}`
+    this.children.push(qr)
+    return qr 
+  }
+  queryMSeg = (route, retries = 2) => {
+    let msqr = new QueryMSeg(this, this.children.length, route, retries)
+    msqr.name = `qr_mseg_${this.children.length}`
+    this.children.push(msqr)
+    return msqr 
+  }
+
+  // root loop is unique, children's requestLoopCycle() all terminate here, 
+  // only schedule once per turn, 
+  loopTimer = null
+  directCallCount = 0 
+  requestLoopCycle = () => {
+    if (!this.loopTimer){
+      this.loopTimer = setTimeout(this.loop, 0)
+    }
+  }
+
+  loop = () => {
+    // cancel old timer & start loop
+    clearTimeout(this.loopTimer)
+    this.loopTimer = null
+    osapLoop(this)
+    // if we have queued a timer, just loop again, 
+    // to some limit... 
+    if(this.loopTimer != null){
+      this.directCallCount ++ 
+      if(this.directCallCount < 16){
+        // call it outright, bypassing timer, 
+        this.loop()
+      } else {
+        // relax, give the js event loop space, 
+        // timer will ensure that we are called in next js event cycle 
+        this.directCallCount = 0 
+      }
+    }
+  }
+
+  // graph search tool;
+  netRunner = new NetRunner(this)
+  nr = this.netRunner 
+  // with handles...
+  connect = this.netRunner.connect
+  // mvc tool, 
+  mvc = new OMVC(this)
+  // we ship MVC msgs from the root node, so their responses arrive here... 
+  destHandler = this.mvc.destHandler
+  // we have a high level hookup, 
+  hl = new HighLevel(this)
+} // end OSAP
diff --git a/system/javascript/osapjs/core/packets.js b/system/javascript/osapjs/core/packets.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd2164e2977527b1e39c00a5221ce57180c30f6e
--- /dev/null
+++ b/system/javascript/osapjs/core/packets.js
@@ -0,0 +1,378 @@
+/*
+packets.js / PK 
+
+packet writing for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from './ts.js'
+
+// diff like... 
+// packet keys, for l0 of packets,
+// PKEYS all need to be on the same byte order, since they're 
+// walked
+// TRANSPORT LAYER
+let PK = {
+  PTR: 240,         // packet pointer (next byte is instruction)
+  DEST: 224,        // have arrived, (next bytes are for recipient)
+  PINGREQ: 192,     // hit me back 
+  PINGRES: 176,     // here's ur ping 
+  SCOPEREQ: 160,    // requesting scope info @ this location 
+  SCOPERES: 144,    // replying to your scope request, 
+  SIB: 16,          // sibling fwds,
+  PARENT: 32,       // parent fwds, 
+  CHILD: 48,        // child fwds, 
+  PFWD: 64,         // forward at this port, to port's partner 
+  BFWD: 80,         // fwd at this bus, to <arg> indice 
+  BBRD: 96,         // broadcast here, to <arg> channel 
+  LLESCAPE: 112,    // pls escape this string-formatted message... 
+}
+
+PK.logPacket = (data, routeOnly = false, trace = true) => {
+  // uint8array-only club, 
+  if (!(data instanceof Uint8Array)) {
+    console.warn(`attempt to log non-uint8array packet, bailing`)
+    console.warn(data)
+    return
+  }
+  // write an output msg, 
+  let msg = ``
+  msg += `PKT: \n`
+  let startByte = 4
+  if (routeOnly) {
+    startByte = 0
+  } else {
+    // alright 1st 4 bytes are TTL and segSize 
+    msg += `timeToLive: ${TS.read16(data, 0)}\n`
+    msg += `segSize: ${TS.read16(data, 2)}\n`
+  }
+  // now we have sets of instructions, 
+  msgLoop: for (let i = startByte; i < data.length; i += 2) {
+    switch (PK.readKey(data, i)) {
+      case PK.PTR:
+        msg += `[${data[i]}] PTR ---------------- v\n`
+        i--;
+        break;
+      case PK.DEST:
+        msg += `[${data[i]}] DEST, DATA LEN: ${data.length - i}`
+        while(i < data.length){
+          i ++ 
+          msg += `\n[${data[i]}]`
+        }
+        break msgLoop;
+      case PK.PINGREQ:
+        msg += `[${data[i]}], [${data[i + 1]}] PING REQUEST: ID: ${PK.readArg(data, i)}`;
+        break msgLoop;
+      case PK.PINGRES:
+        msg += `[${data[i]}], [${data[i + 1]}] PING RESPONSE: ID: ${PK.readArg(data, i)}`
+        break msgLoop;
+      case PK.SCOPEREQ:
+        msg += `[${data[i]}], [${data[i + 1]}] SCOPE REQUEST: ID: ${PK.readArg(data, i)}`
+        break msgLoop;
+      case PK.SCOPERES:
+        msg += `[${data[i]}], [${data[i + 1]}] SCOPE RESPONSE: ID: ${PK.readArg(data, i)}`
+        break msgLoop;
+      case PK.SIB:
+        msg += `[${data[i]}], [${data[i + 1]}] SIB FWD: IND: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.PARENT:
+        msg += `[${data[i]}], [${data[i + 1]}] PARENT FWD: IND: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.CHILD:
+        msg += `[${data[i]}], [${data[i + 1]}] CHILD FWD: IND: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.PFWD:
+        msg += `[${data[i]}], [${data[i + 1]}] PORT FWD: IND: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.BFWD:
+        msg += `[${data[i]}], [${data[i + 1]}] BUS FWD: RXADDR: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.BBRD:
+        msg += `[${data[i]}], [${data[i + 1]}] BUS BROADCAST: CHANNEL: ${PK.readArg(data, i)}\n`
+        break;
+      case PK.LLESCAPE:
+        msg += `[${data[i]}] LL ESCAPE, STRING LEN: ${data.length - i}`
+        break msgLoop;
+      default:
+        msg += "BROKEN"
+        break msgLoop;
+    }
+  } // end of loop-thru, 
+  console.log(msg)
+  if(trace) console.trace()
+}
+
+PK.logRoute = (route, trace = true) => {
+  let pckt = PK.writeDatagram(route, new Uint8Array(0))
+  PK.logPacket(pckt, false, trace)
+}
+
+// idiosyncrasy related to old-style vm route-building vs... new-style route-searching algos...
+PK.VC2VMRoute = (route) => {
+  // we can't do this to caller's route object, so we make a copy, 
+  route = PK.route(route).end()
+  // i.e. search routines return paths from browser-root node, to the root node in the remote 
+  // object... but vms are written to go from a *child* of the browser root node, to *children* in the remote...
+  // departing from a sibling, not the parent... 
+  route.path[1] = PK.SIB
+  // and not going *up* to the parent, once traversing into the context... 
+  route.path = new Uint8Array(route.path.subarray(0, route.path.length - 2))
+  return route 
+}
+
+PK.VC2EPRoute = (route) => {
+  // we can't do this to caller's route object, so we make a copy, 
+  route = PK.route(route).end()
+  // i.e. search routines return paths from browser-root node, to the root node in the remote 
+  // object... but vms are written to go from a *child* of the browser root node, to *children* in the remote...
+  // departing from a sibling, not the parent... 
+  route.path[1] = PK.SIB
+  // that's it for these, 
+  return route 
+}
+
+PK.route = (existing) => {
+  // start w/ a temp uint8 array, 
+  let path = new Uint8Array(256)
+  let wptr = 0
+  // copy-in existing path, if starting from some root, 
+  if (existing != null && existing.path != undefined) {
+    path.set(existing.path, 0)
+    wptr = existing.path.length
+  } else {
+    path[wptr++] = PK.PTR
+  }
+  // add & return this, to chain... 
+  return {
+    sib: function (indice) {
+      indice = parseInt(indice)
+      PK.writeKeyArgPair(path, wptr, PK.SIB, indice)
+      wptr += 2
+      return this
+    },
+    parent: function () {
+      PK.writeKeyArgPair(path, wptr, PK.PARENT, 0)
+      wptr += 2
+      return this
+    },
+    child: function (indice) {
+      indice = parseInt(indice)
+      PK.writeKeyArgPair(path, wptr, PK.CHILD, indice)
+      wptr += 2
+      return this
+    },
+    pfwd: function () {
+      PK.writeKeyArgPair(path, wptr, PK.PFWD, 0)
+      wptr += 2
+      return this
+    },
+    bfwd: function (indice) {
+      indice = parseInt(indice)
+      PK.writeKeyArgPair(path, wptr, PK.BFWD, indice)
+      wptr += 2
+      return this
+    },
+    bbrd: function (channel) {
+      channel = parseInt(channel)
+      PK.writeKeyArgPair(path, wptr, PK.BBRD, channel)
+      wptr += 2
+      return this
+    },
+    end: function (ttl, segSize, noOpt = false) {
+      // we want to absorb ttl & segSize from existing if it was used, 
+      // but also *not* of ttl and segSize are used here, 
+      if(existing != null && existing.ttl && existing.segSize){
+        ttl = existing.ttl
+        segSize = existing.segSize
+      } else {
+        ttl = 1000
+        segSize = 128 
+      }
+      // we also want to abbreviate the non-optimal parent().child() pattern 
+      // that emerges during sweeps / etc, 
+      if(!noOpt){
+        for(let ptr = 1; ptr < path.length - 4; ptr += 2){
+          if(PK.readKey(path, ptr) == PK.PARENT && PK.readKey(path, ptr + 2) == PK.CHILD){
+            // console.log(`found non-opt at ${ptr}`, JSON.parse(JSON.stringify(path.subarray(ptr, ptr + 4))))
+            // child arg = sib arg, 
+            let sibIndice = PK.readArg(path, ptr + 2)
+            // console.log(`sib is ${sibIndice}`)
+            // insert, 
+            PK.writeKeyArgPair(path, ptr, PK.SIB, sibIndice)
+            // path is uint8array, so we have to shift back like so... 
+            wptr -= 2
+            for(let i = ptr + 2; i < wptr; i ++){
+              path[i] = path[i + 2]
+            }
+          }
+        }
+      }
+      // return a path object, 
+      return {
+        ttl: ttl, 
+        segSize: segSize,
+        path: new Uint8Array(path.subarray(0, wptr)),
+      }
+    }
+  }
+}
+
+// match on route objects, 
+PK.routeMatch = (ra, rb) => {
+  if(ra.path.length != rb.path.length) return false; 
+  for(let i = 0; i < ra.path.length; i ++){
+    if(ra.path[i] != rb.path[i]) return false;
+  }
+  return true
+}
+
+// where route = { ttl: <num>, segSize: <num>, path: <uint8array> }
+PK.writeDatagram = (route, payload) => {
+  let datagram = new Uint8Array(route.path.length + payload.length + 4)
+  TS.write('uint16', route.ttl, datagram, 0)
+  TS.write('uint16', route.segSize, datagram, 2)
+  datagram.set(route.path, 4)
+  datagram.set(payload, 4 + route.path.length)
+  if(datagram.length > route.segSize) throw new Error(`writing datagram of len ${datagram.length} w/ segSize setting ${segSize}`);
+  return datagram
+}
+
+PK.writeReply = (ogPck, payload) => {
+  // find the pointer, 
+  let ptr = PK.findPtr(ogPck)
+  if (!ptr) throw new Error(`during reply-write, couldn't find the pointer...`);
+  // our new datagram will be this long (ptr is location of ptr, len is there + 1) + the payload length, so 
+  let datagram = new Uint8Array(ptr + 1 + payload.length)
+  // we're using the OG ttl and segsize, so we can just write that in, 
+  datagram.set(ogPck.subarray(0, 4))
+  // and also write in the payload, which will come after the ptr's current position, 
+  datagram.set(payload, ptr + 1)
+  // now we want to do the walk-to-ptr, reversing... 
+  // we write at the head of the packet, whose first byte is the pointer, 
+  let wptr = 4
+  datagram[wptr++] = PK.PTR
+  // don't write past here, 
+  let end = ptr 
+  // read from the back, 
+  let rptr = ptr
+  walker: for (let h = 0; h < 16; h++) {
+    if(wptr >= end) break walker;
+    rptr -= 2
+    switch (PK.readKey(ogPck, rptr)) {
+      case PK.SIB:
+      case PK.PARENT:
+      case PK.CHILD:
+      case PK.PFWD:
+      case PK.BFWD:
+      case PK.BBRD:
+        // actually we can do the same action for each of these keys, 
+        datagram.set(ogPck.subarray(rptr, rptr + 2), wptr)
+        wptr += 2
+        break;
+      default:
+        throw new Error(`during writeReply route reversal, encountered unpredictable key ${ogPck[rptr]}`)
+    }
+  }
+  // that's it, 
+  return datagram
+}
+
+// returns the position of the ptr key such that pck[ptr] == PK.PTR, or undefined 
+PK.findPtr = (pck) => {
+  // 1st position the ptr can be in is 4, 
+  let ptr = 4
+  // search fwd for a max of 16 steps, 
+  for (let h = 0; h < 16; h++) {
+    switch (PK.readKey(pck, ptr)) {
+      case PK.PTR:    // it's the ptr, return it
+        return ptr
+      case PK.SIB:    // keys which could be between start of pckt and terminal, 
+      case PK.PARENT:
+      case PK.CHILD:
+      case PK.PFWD:
+      case PK.BFWD:
+      case PK.BBRD:
+        ptr += 2
+        break;
+      default:        // anything else means a broken packet, 
+        return undefined
+    }
+  }
+}
+
+// walks the ptr ahead by n steps, putting reversed instructions behind, 
+PK.walkPtr = (pck, ptr, source, steps) => {
+  // check check... 
+  if (pck[ptr] != PK.PTR) { throw new Error(`bad ptr walk, pck[ptr] == ${pck[ptr]} not PK.PTR`) }
+  // walk along, switching on instructions... 
+  for (let h = 0; h < steps; h++) {
+    switch (PK.readKey(pck, ptr + 1)) {
+      case PK.SIB: {
+          // stash indice of from-whence it came, 
+          let txIndice = source.indice 
+          // track for this loop's next step, before we modify the packet data
+          source = source.parent.children[PK.readArg(pck, ptr + 1)]
+          // so, where ptr is currently goes the new key / arg pair for a reversal, 
+          // for a sibling pass, that's the sibling to pass back to, 
+          PK.writeKeyArgPair(pck, ptr, PK.SIB, txIndice)
+          // then the position +2 from current ptr becomes the ptr, now it's just behind the next instruction, 
+          pck[ptr + 2] = PK.PTR
+          ptr += 2
+        }
+        break;
+      case PK.PARENT:
+        // reversal for a 'parent' instruction is to go back to the child, 
+        PK.writeKeyArgPair(pck, ptr, PK.CHILD, source.indice)
+        pck[ptr + 2] = PK.PTR
+        // next source... 
+        source = source.parent
+        ptr += 2
+        break;
+      case PK.CHILD:
+        // next src will be 
+        source = source.children[PK.readArg(pck, ptr + 1)]
+        // reversal for a 'child' instruction is to go back to the parent, 
+        PK.writeKeyArgPair(pck, ptr, PK.PARENT, 0)
+        pck[ptr + 2] = PK.PTR
+        ptr += 2
+        break;
+      case PK.PFWD:
+        // reversal for a pfwd is just a pointer hop, 
+        PK.writeKeyArgPair(pck, ptr, PK.PFWD, 0)
+        pck[ptr + 2] = PK.PTR
+        // PFWD is a network instruction, we should only ever be ptr-walking once in this case, 
+        if (steps != 1) throw new Error(`likely bad call to walkPtr, we have port-fwd here w/ more than 1 step`)
+        return;
+      case PK.BFWD:
+      case PK.BBRD:
+        throw new Error(`bus instructions in JS, badness`)
+        break;
+      case PK.PTR:    // this doesn't make any sense, we had pck[ptr] = PK.PTR, and are here at pck[ptr + 1]
+      default:        // anything else means a broken instruction, 
+        throw new Error(`out of place keys during a pointer increment`)
+    }
+  }
+}
+
+PK.readKey = (data, start) => {
+  return data[start] & 0b11110000
+}
+
+// we use strange-endianness for arguments, 
+PK.readArg = (data, start) => {
+  return ((data[start] & 0b00001111) << 8) | data[start + 1]
+}
+
+PK.writeKeyArgPair = (data, start, key, arg) => {
+  data[start] = key | (0b00001111 & (arg >> 8))
+  data[start + 1] = arg & 0b11111111
+}
+
+export default PK 
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/query.js b/system/javascript/osapjs/core/query.js
new file mode 100644
index 0000000000000000000000000000000000000000..bcb27c9fc59cebeeb93510bcc024065a164f2d80
--- /dev/null
+++ b/system/javascript/osapjs/core/query.js
@@ -0,0 +1,90 @@
+/*
+osapQuery.js
+
+resolves remote data for local code 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { VT, EP } from './ts.js'
+import TIME from './time.js'
+import PK from './packets.js'
+import Vertex from './vertex.js'
+
+export default class Query extends Vertex {
+  constructor(parent, indice, route, retries) {
+    super(parent, indice)
+    this.route = route
+    this.maxRetries = retries 
+  }
+
+  type = VT.QUERY
+  
+  // ---------------------------------- Some State, as a Treat 
+
+  queryAwaiting = null 
+  runningQueryID = 101
+
+  // ---------------------------------- Reply Catch Side 
+
+  destHandler = function (item, ptr) {
+    // again, item.data[ptr] == PK.PTR, ptr + 1 = PK.DEST, ptr + 2 = EP.QUERY_RES,
+    switch (item.data[ptr + 2]) {
+      case EP.QUERY_RES:
+        // match & bail 
+        if(this.queryAwaiting.id == item.data[ptr + 3]){
+          clearTimeout(this.queryAwaiting.timeout)
+          for(let res of this.queryAwaiting.resolutions){
+            res(new Uint8Array(item.data.subarray(ptr + 4)))
+          }
+          this.queryAwaiting = null 
+        } else {
+          console.error('on query reply, no matching resolution')
+        }
+        break;
+      default:
+        console.error('root recvs data / not query resp')
+    }
+    item.handled() 
+  }
+
+  // ---------------------------------- Issuing Side 
+
+  pull = () => {
+    return new Promise((resolve, reject) => {
+      if (this.queryAwaiting) {
+        this.queryAwaiting.resolutions.push(resolve)
+      } else {
+        let queryID = this.runningQueryID 
+        this.runningQueryID ++; this.runningQueryID = this.runningQueryID & 0b11111111; 
+        let datagram = PK.writeDatagram(this.route, new Uint8Array([PK.DEST, EP.QUERY, queryID]))
+        this.queryAwaiting = {
+          id: queryID,
+          resolutions: [resolve],
+          retries: 0,
+          timeoutFn: () => {
+            if(this.queryAwaiting.retries >= this.maxRetries){
+              this.queryAwaiting = null
+              reject(`query timeout after ${this.maxRetries} retries`)  
+            } else {
+              console.warn(`query retry`)
+              this.queryAwaiting.retries ++ 
+              this.handle(datagram, VT.STACK_ORIGIN)
+              this.queryAwaiting.timeout = setTimeout(this.queryAwaiting.timeoutFn, TIME.staleTimeout)
+            }
+          }
+        } // end query obj 
+        // set 1st timeout, 
+        this.queryAwaiting.timeout = setTimeout(this.queryAwaiting.timeoutFn,TIME.staleTimeout)
+        // parent handles,
+        this.handle(datagram, VT.STACK_ORIGIN)
+      }
+    })
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/queryMSeg.js b/system/javascript/osapjs/core/queryMSeg.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d969a33309a084739629af09fb4a06c697795ea
--- /dev/null
+++ b/system/javascript/osapjs/core/queryMSeg.js
@@ -0,0 +1,84 @@
+/*
+osapQueryMSeg.js
+
+resolves remote data for local code, big chonkers 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { EPMSEG, TS, EP } from './ts.js'
+import Vertex from './vertex.js'
+
+export default class QueryMSeg extends Vertex {
+  constructor(parent, indice, route, retries) {
+    super(parent, indice)
+    this.route = route 
+    this.maxRetries = retries 
+  }
+
+  // ---------------------------------- Catch Side 
+
+  destHandler = function (item, ptr) {
+    let data = item.data 
+    let startByte = 0
+    let endByte = 0 
+    let terminal = false 
+    ptr += 3 
+    switch(data[ptr]){
+      case EPMSEG.QUERY_END_RESP:
+        terminal = true 
+      case EPMSEG.QUERY_RES:
+        // bytes -> bytes 
+        startByte = TS.read('uint16', data, ptr + 1)
+        endByte = TS.read('uint16', data, ptr + 3)
+        //console.log(startByte, endByte)
+        if(startByte == 0) this.tempData = [] 
+        let i = 0 
+        for(let b = startByte; b < endByte; b ++){
+          this.tempData[b] = data[ptr + 5 + (i ++)]
+        }
+        if(!terminal){
+          this.reqNewSeg(endByte)
+          clearTimeout(this.rejectTimeout)
+          this.rejectTimeout = setTimeout(() => {this.pullReject('mseg timeout')}, 1000)
+        } else {
+          this.pullResolve(this.tempData)
+          clearTimeout(this.rejectTimeout)
+        }
+    }
+    return true 
+  }
+
+  tempData = {}
+
+  reqNewSeg = (start) => {
+    //console.log(`req at ${start}`)
+    // len is [route][querykey][start:2][end:2]
+    let req = new Uint8Array(this.route.length + 5)
+    req.set(this.route, 0)
+    let wptr = this.route.length 
+    req[wptr ++] = EPMSEG.QUERY;
+    wptr += TS.write('uint16', start, req, wptr)
+    wptr += TS.write('uint16', start + 64, req, wptr)
+    this.handle(req, 0)
+  }
+
+  pullResolve = null 
+  pullReject = null 
+  rejectTimeout = null 
+
+  pull = () => {
+    return new Promise((resolve, reject) => {
+      this.pullResolve = resolve 
+      this.pullReject = reject 
+      this.reqNewSeg(0)
+      this.rejectTimeout = setTimeout(() => {this.pullReject('mseg timeout')}, 1000)
+    })
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/time.js b/system/javascript/osapjs/core/time.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9c23a006fae8ca2cd2bfc07abfc7443d688aa7a
--- /dev/null
+++ b/system/javascript/osapjs/core/time.js
@@ -0,0 +1,54 @@
+/*
+times.js
+
+time utilities for OSAP 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// nice ute for async functions... 
+let TIME = {}
+
+let getTimeStamp = null
+
+if (typeof process === 'object') {
+  const { PerformanceObserver, performance } = require('perf_hooks')
+  getTimeStamp = () => {
+    return performance.now()
+  }
+} else {
+  getTimeStamp = () => {
+    return performance.now()
+  }
+}
+
+TIME.getTimeStamp = () => { return getTimeStamp() }
+
+TIME.staleTimeout = 1000 
+
+TIME.delay = (ms) => {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => { resolve() }, ms)
+  })
+}
+
+TIME.awaitFutureTime = (systemMs) => {
+  return new Promise((resolve, reject) => {
+    let check = () => {
+      if(TIME.getTimeStamp() >= systemMs){
+        resolve()
+      } else {
+        setTimeout(check, 0)
+      }
+    } 
+    check()
+  })
+}
+
+export default TIME 
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/ts.js b/system/javascript/osapjs/core/ts.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2044931abec223f228c5031fa3328ee2d62998b
--- /dev/null
+++ b/system/javascript/osapjs/core/ts.js
@@ -0,0 +1,191 @@
+/*
+ts.js // typeset
+
+serialization & keys for OSAP
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// vertex types & keys 
+let VT = {
+  ROOT: 22,
+  MODULE: 23,
+  ENDPOINT: 24,
+  QUERY: 25,
+  VPORT: 44,
+  VBUS: 45,
+  STACK_ORIGIN: 0,
+  STACK_DEST: 1
+}
+
+VT.defaultStackSize = 5
+
+// endpoint layer types & keys 
+let EP = {
+  SS_ACK: 101,      // the ack, 
+  SS_ACKLESS: 121,  // ackless transmit 
+  SS_ACKED: 122,    // transmit requests ack 
+  QUERY: 131,       // query for current data 
+  QUERY_RES: 132,  // query response 
+  ROUTE_QUERY_REQ: 141, // request route list 
+  ROUTE_QUERY_RES: 142,  // route list, 
+  ROUTE_SET_REQ: 143,    // req-to-add-route 
+  ROUTE_SET_RES: 144,
+  ROUTE_RM_REQ: 147,
+  ROUTE_RM_RES: 148,
+  ROUTEMODE_ACKED: 167,
+  ROUTEMODE_ACKLESS: 168,
+}
+
+// vbus layer keys 
+let VBUS =  {
+  BROADCAST_MAP_REQ: 145,
+  BROADCAST_MAP_RES: 146,
+  BROADCAST_QUERY_REQ: 141,
+  BROADCAST_QUERY_RES: 142,
+  BROADCAST_SET_REQ: 143,
+  BROADCAST_SET_RES: 144,
+  BROADCAST_RM_REQ: 147,
+  BROADCAST_RM_RES: 148 
+}
+
+let EPMSEG = {
+  QUERY: 141,
+  QUERY_RES: 142,
+  QUERY_END_RESP: 143
+}
+
+// the 'typeset' 
+let TS = {}
+
+// just shorthands, 
+TS.read16 = (data, start) => {
+  return TS.read('int16', data, start)
+}
+
+TS.write16 = (value, data, start) => {
+  TS.write('uint16', value, data, start)
+}
+
+let decoder = new TextDecoder()
+// let tempRead = {} 
+
+TS.read = (type, data, start) => {
+  // uint8array-only club, 
+  if (!(data instanceof Uint8Array)) {
+    console.warn(`attempt to read from non-uint8array data, bailing`)
+    console.warn(data)
+    return
+  }
+  // read it... 
+  switch (type) {
+    case 'int32':
+      return new Int32Array(data.buffer.slice(start, start + 4))[0]
+    case 'uint8':
+      return new Uint8Array(data.buffer.slice(start, start + 1))[0]
+    case 'int16':
+      return new Int16Array(data.buffer.slice(start, start + 2))[0]
+    case 'uint16':
+      return new Uint16Array(data.buffer.slice(start, start + 2))[0]
+    case 'uint32':
+      return new Uint32Array(data.buffer.slice(start, start + 4))[0]
+    case 'float32':
+      return new Float32Array(data.buffer.slice(start, start + 4))[0]
+    case 'boolean':
+      if (data[start] > 0) {
+        return true
+      } else {
+        return false
+      }
+      break;
+    case 'string':
+      let length = (data[start] & 255) | (data[start + 1] << 8) | (data[start + 2] << 16) | (data[start + 3] << 24)
+      let pckSlice = data.buffer.slice(start + 4, start + 4 + length)
+      return {
+        value: decoder.decode(pckSlice),
+        inc: length + 4
+      }
+    default:
+      console.error('no code for this type read')
+      return null
+      break;
+  }
+}
+
+let encoder = new TextEncoder()
+let tempArr = {}
+let tempBytes = {}
+
+TS.write = (type, value, data, start) => {
+  // uint8arrays-only club, 
+  if (!(data instanceof Uint8Array)) {
+    console.warn(`attempt to write into non-uint8array packet, bailing`)
+    console.warn(data)
+    return
+  }
+  // write types... 
+  switch (type) {
+    case 'uint8':
+      data[start] = value & 255
+      return 1
+    case 'uint16':
+      // little endian: lsb is at the lowest address
+      data[start] = value & 255
+      data[start + 1] = (value >> 8) & 255
+      return 2
+    case 'int32':
+      tempArr = Int32Array.from([value])
+      tempBytes = new Uint8Array(tempArr.buffer)
+      data.set(tempBytes, start)
+      return 4
+    case 'uint32':
+      data[start] = value & 255
+      data[start + 1] = (value >> 8) & 255
+      data[start + 2] = (value >> 16) & 255
+      data[start + 3] = (value >> 24) & 255
+      return 4
+    case 'float32':
+      tempArr = Float32Array.from([value])
+      tempBytes = new Uint8Array(tempArr.buffer)
+      data.set(tempBytes, start)
+      return 4
+    case 'char':
+      //      console.log('char', value.charCodeAt(0))
+      data[start] = value.charCodeAt(0)
+      return 1
+    case 'string': // so, would be good to send long strings (i.e. dirty old gcodes), so 32b base
+      let stringStream = encoder.encode(value)
+      //console.log("WRITING STRING", value)
+      data[start] = stringStream.length & 255
+      data[start + 1] = (stringStream.length >> 8) & 255
+      data[start + 2] = (stringStream.length >> 16) & 255
+      data[start + 3] = (stringStream.length >> 24) & 255
+      data.set(stringStream, start + 4)
+      return 4 + stringStream.length
+    case 'boolean':
+      if (value) {
+        data[start] = 1
+      } else {
+        data[start] = 0
+      }
+      return 1
+    default:
+      console.error('no code for this type write')
+      return null
+      break;
+  }
+}
+
+export {
+  TS,     // typeset 
+  VT,     // object types 
+  EP,     // endpoint keys 
+  EPMSEG, // mseg endpoint keys,
+  VBUS,   // vbus mvc keys 
+}
diff --git a/system/javascript/osapjs/core/vertex.js b/system/javascript/osapjs/core/vertex.js
new file mode 100644
index 0000000000000000000000000000000000000000..ec1fd5d4c2899170448f5fa7ef06953b37e3b6d5
--- /dev/null
+++ b/system/javascript/osapjs/core/vertex.js
@@ -0,0 +1,309 @@
+/*
+vertex.js
+
+base vertex in osap graph 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { VT, TS } from './ts.js'
+import TIME from './time.js'
+import PK from './packets.js'
+
+export default class Vertex {
+  constructor(parent, indice) {
+    this.parent = parent
+    this.indice = indice
+  }
+
+  name = "vt_unnamed"
+  children = [] // all have some children array, not all have children 
+  scopeTimeTag = 0 // this property is helpful when looking across graphs, 
+
+  // should be extended... 
+  // pls don't forget to item.handled(), or don't if you want it to hang & return next loop 
+  destHandler = function (item, ptr) {
+    console.log(`default vertex type ${this.type} indice ${this.indice} destHandler`)
+    item.handled()
+  }
+
+  // ------------------------------------------------------ Stacks 
+
+  // we keep a stack of messages... 
+  maxStackLength = VT.defaultStackSize
+  stack = [[], []]
+
+  // can check availability, we use this for FC 
+  stackAvailableSpace = (od) => {
+    if (od > 2 || od == undefined) { console.error("bad od arg"); return 0 }
+    return (this.maxStackLength - this.stack[od].length)
+  }
+
+  awaitStackAvailableSpace = (od, timeout = 1000, count = 1) => {
+    return new Promise((resolve, reject) => {
+      let to = setTimeout(() => {
+        reject('await stack available space timeout')
+      }, timeout)
+      let check = () => {
+        if (this.stackAvailableSpace(od) >= count) {
+          clearTimeout(to)
+          resolve()
+        } else {
+          // TODO: curious to watch if this occurs, so: (should delete later)
+          console.warn('stack await space...')
+          setTimeout(check, 0)
+        }
+      }
+      check()
+    })
+  }
+
+  // ------------------------------------------------------ Data Ingest 
+  // this is the data uptake, 
+  handle = (data, od = VT.STACK_ORIGIN) => {
+    // no-not-buffers club, 
+    if (!(data instanceof Uint8Array)) {
+      console.error(`non-uint8_t ingest at handle, rejecting`)
+      return
+    } else if (od == null || od > 2) {
+      console.error(`bad od argument ${od} at handle`)
+      return
+    }
+    let item = {}
+    item.data = new Uint8Array(data)                  // copy in, old will be gc 
+    item.arrivalTime = TIME.getTimeStamp()           // track arrival time 
+    item.timeToLive = TS.read('uint16', item.data, 0) // track TTL, 
+    item.vt = this                                    // handle to us, 
+    item.od = od                                      // which stack... 
+    item.handled = () => {
+      //console.warn(`handled from ${od} stack at ${this.indice}`)
+      let ok = false
+      for (let i in this.stack[od]) {
+        if (this.stack[od][i] == item) {
+          this.stack[od].splice(i, 1)
+          ok = true
+          break;
+        }
+      }
+      if (!ok) console.error("bad stack search") //throw new Error("on handled, item not present")
+    }
+    this.stack[od].push(item)
+    // try this log out for a fun time / to see how inefficient / non-parallel you code can be...
+    //if(this.name == "ep_axlStateMirror") console.log(`pushed to stack ${od} size ${this.stack[od].length}`)
+    this.requestLoopCycle()
+  }
+
+  // handle to kick loops, passes up parent chain to root 
+  requestLoopCycle = () => {
+    this.parent.requestLoopCycle()
+  }
+
+  // ------------------------------------------------------ PING 
+  // any vertex can issue a ping to a route...
+  runningPingID = 42
+  pingsAwaiting = []
+
+  ping = async (route) => {
+    try {
+      // record ping start time, 
+      let startTime = TIME.getTimeStamp()
+      await this.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+      // track an id & increment / wrap tracker,
+      let id = this.runningPingID
+      this.runningPingID++; this.runningPingID = this.runningPingID & 0b11111111;
+      // payload is just the request & its id, 
+      let payload = new Uint8Array([PK.PINGREQ, id])
+      // write a 'gram from that, then have vertex ingest it, 
+      let datagram = PK.writeDatagram(route, payload)
+      this.handle(datagram, VT.STACK_ORIGIN)
+      // resolve when the ping comes back, 
+      return new Promise((resolve, reject) => {
+        this.pingsAwaiting.push({
+          startTime: startTime,
+          res: resolve,
+          id: id,
+        })
+        setTimeout(() => {
+          reject(`ping timed out after 5s`)
+        }, 5000)
+      })
+    } catch (err) {
+      throw err
+    }
+  }
+
+  pingRequestHandler = (item, ptr) => {
+    // item.data[ptr] == PK.PTR 
+    // we want to ack this... basically without modifying anything,
+    let id = item.data[ptr + 2]
+    let datagram = PK.writeReply(item.data, [PK.PINGRES, id])
+    // we'll ack "in place" by rm-ing this item from the destination stack & then replacing it, 
+    // no checks this way: pings and scope are always answered, even if i.e. single-stack endpoint
+    // is on an every-loop-update, etc... 
+    item.handled()
+    //PK.logPacket(datagram)
+    //console.log(item.vt.name)
+    this.handle(datagram, VT.STACK_DEST)
+  }
+
+  pingResponseHandler = (item, ptr) => {
+    // item.data[ptr] = PK.PTR, ptr + 1 == PK.PINGRES 
+    let id = PK.readArg(item.data, ptr + 1)
+    for (let a = 0; a < this.pingsAwaiting.length; a++) {
+      if (this.pingsAwaiting[a].id == id) {
+        let pa = this.pingsAwaiting[a]
+        pa.res(TIME.getTimeStamp() - pa.startTime)
+        this.pingsAwaiting.splice(a, 1)
+      }
+    }
+    item.handled()
+  }
+
+  // ------------------------------------------------------ SCOPE 
+  // any vertex can request 'scope' info from some partner, 
+  runningScopeID = 11
+  scopesAwaiting = []
+
+  scope = async (route, timeTag) => {
+    try {
+      if (!timeTag) {
+        console.warn("scope called w/ no timeTag")
+        timeTag = 0
+      }
+      // maybe a nice API in general is like 
+      // (1) wait for outgoing space in the root's origin stack: 
+      await this.awaitStackAvailableSpace(VT.STACK_ORIGIN)
+      // (2) write a packet, just the scope request, to whatever route, w/ a unique-ish id, 
+      let id = this.runningScopeID
+      this.runningScopeID++; this.runningScopeID = this.runningScopeID & 0b11111111;
+      // payload is just request key, ID, and uint32_t timeTag, 
+      let payload = new Uint8Array(6)
+      payload[0] = PK.SCOPEREQ; payload[1] = id;
+      TS.write('uint32', timeTag, payload, 2)
+      // can write a datagram w/ the route & payload
+      let datagram = PK.writeDatagram(route, payload)
+      // (3) send the packet !
+      this.handle(datagram, VT.STACK_ORIGIN)
+      // (4) setup to handle the request, associating it w/ this fn  
+      // about timeout math: we have a route which has route.length / 2 operations, and each has a ttl of our given ttl, 
+      // and we have a round-trip, so absolute maximum time it could take is... that count * that time, 
+      return new Promise((resolve, reject) => {
+        // timeouts... should be appropriately long, but not ultra-long, 
+        let toTime = Math.min(route.path.length * route.ttl, 2000)
+        this.scopesAwaiting.push({
+          request: new Uint8Array(datagram),            // copy-in the og request 
+          id: id,                                       // it's id 
+          timeout: setTimeout(() => {                   // a timeout... 
+            reject(`scope timeout`)
+          }, toTime),
+          onResponse: function (item, ptr) {            // callback / handler 
+            try {
+              // clear timeout 
+              clearTimeout(this.timeout)
+              // now we want to resolve this w/ a description of the ...
+              // virtual vertex ? vvt ? ffs. 
+              let vvt = {}
+              let rptr = ptr + 3
+              vvt.route = route
+              vvt.timeTag = timeTag // what we just tagged it with 
+              vvt.previousTimeTag = TS.read('uint32', item.data, rptr) // what it replies w/ as previous tag 
+              rptr += 4
+              vvt.type = item.data[rptr++]
+              if (vvt.type == VT.VPORT) {
+                vvt.linkState = (item.data[rptr++] > 0 ? true : false);
+              } else if (vvt.type == VT.VBUS) {
+                // first we get uint16_t num-addresses, 
+                vvt.linkState = new Array(TS.read('uint16', item.data, rptr))
+                rptr += 2
+                vvt.reciprocals = new Array(vvt.linkState.length)
+                vvt.ownRxAddr = TS.read('uint16', item.data, rptr)
+                rptr += 2
+                let bitByteModulo = 0
+                for (let l = 0; l < vvt.linkState.length; l++) {
+                  vvt.linkState[l] = (item.data[rptr] & (1 << bitByteModulo) ? true : false)
+                  bitByteModulo++
+                  if (bitByteModulo >= 8) {
+                    bitByteModulo = 0
+                    rptr++
+                  }
+                }
+                //console.warn(vvt.type, vvt.linkState)
+              } else {
+                vvt.linkState = false
+              }
+              vvt.indice = TS.read('uint16', item.data, rptr); rptr += 2
+              vvt.numSiblings = TS.read('uint16', item.data, rptr); rptr += 2
+              vvt.children = new Array(TS.read('uint16', item.data, rptr)); rptr += 2
+              vvt.name = TS.read('string', item.data, rptr).value
+              resolve(vvt)
+            } catch (err) {
+              console.error(err)
+            }
+          }
+        })
+      })
+    } catch (err) {
+
+    }
+  }
+
+  scopeResponseHandler = (item, ptr) => {
+    // search for tailing by id...
+    let id = PK.readArg(item.data, ptr + 1)
+    for (let a = 0; a < this.scopesAwaiting.length; a++) {
+      if (this.scopesAwaiting[a].id == id) {
+        this.scopesAwaiting[a].onResponse(item, ptr)
+        this.scopesAwaiting.splice(a, 1)
+      }
+    }
+    item.handled()
+  }
+
+  scopeRequestHandler = (item, ptr) => {
+    // replying to this thing... we have item.data[ptr] == PK.PTR 
+    let id = PK.readArg(item.data, ptr + 1)
+    // +1 for the key, +1 for the id, +4 for the time tag, +1 for type, 
+    // +1 if we are vport-type, 
+    // +2 for own indice, +2 for # siblings, +2 for # children
+    // + string name length + 4 counts for string's length
+    let payload = new Uint8Array(13 + this.name.length + 4 + (this.type == VT.VPORT ? 1 : 0))
+    // write the key & id, 
+    PK.writeKeyArgPair(payload, 0, PK.SCOPERES, id)
+    // to write the rest, we'll wptr +=... starting from 2, 
+    let wptr = 2
+    // the time we were last scoped:
+    wptr += TS.write('uint32', this.scopeTimeTag, payload, wptr)
+    // and read-in the new scope time data 
+    this.scopeTimeTag = TS.read('uint32', item.data, ptr + 3)
+    // our type, 
+    payload[wptr++] = this.type
+    // vport / vbus writes link states, 
+    if (this.type == VT.VPORT) {
+      payload[wptr++] = (this.isOpen() ? 1 : 0);
+    } else if (this.type == VT.VBUS) {
+      throw new Error("scopeRequestHandler at vbus in JS, now you have to write this code")
+    }
+    // our own indice, # of siblings, # of children:
+    wptr += TS.write('uint16', this.indice, payload, wptr)
+    if (this.parent) {
+      wptr += TS.write('uint16', this.parent.children.length, payload, wptr)
+    } else {
+      wptr += TS.write('uint16', 0, payload, wptr)
+    }
+    wptr += TS.write('uint16', this.children.length, payload, wptr)
+    // finally, our name:
+    wptr += TS.write('string', this.name, payload, wptr)
+    // console.log('scope response generated:', payload, wptr)
+    // write the full packet,
+    let datagram = PK.writeReply(item.data, payload)
+    // we can put this back in the same stack slot, clearing the OG and replacing, 
+    item.handled()
+    this.handle(datagram, VT.STACK_DEST)
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/core/vport.js b/system/javascript/osapjs/core/vport.js
new file mode 100644
index 0000000000000000000000000000000000000000..85a9bd2c2e3d5632f2e3751c8f402b444db03149
--- /dev/null
+++ b/system/javascript/osapjs/core/vport.js
@@ -0,0 +1,51 @@
+/*
+vport.js
+
+virtual port, for osap
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { VT } from './ts.js'
+import Vertex from './vertex.js'
+
+export default class VPort extends Vertex {
+  constructor(parent, indice) {
+    super(parent, indice)
+  }
+
+  /* to implement */
+  // write this.cts(), returning whether / not thing is open & clear to send 
+  // write this.send(buffer), putting bytes on the line 
+  // on data, call this.recieve(buffer) with a uint8array arg 
+
+  name = "unnamed vport"
+  maxSegLength = 128
+  type = VT.VPORT
+
+  // phy implements these;
+  // is the connection open ? link state 
+  isOpen = function () { throw new Error(`vport ${this.name} hasn't implemented an isOpen() function`) }
+  // is it clear to send ? flow control 
+  cts = function () { throw new Error(`vport ${this.name} hasn't implemented a cts() function`) }
+  // send this... 
+  send = function (buffer) { throw new Error(`vport ${this.name} hasn't implemented a send() function`) }
+
+  // phy uses this on receipt of a datagram, to ingest to OSAP 
+  receive = function (buffer) {
+    // datagram goes straight through 
+    this.handle(buffer, VT.STACK_ORIGIN)
+  }
+
+  // rm self from osap instance, 
+  dissolve = function () {
+    this.parent.children.splice(this.parent.children.findIndex(elem => elem == this), 1)
+  }
+
+} // end vPort def
diff --git a/system/javascript/osapjs/deprecated/osap-render.js b/system/javascript/osapjs/deprecated/osap-render.js
new file mode 100644
index 0000000000000000000000000000000000000000..cba92658f2a9e6de10780af255fb47954c89f474
--- /dev/null
+++ b/system/javascript/osapjs/deprecated/osap-render.js
@@ -0,0 +1,527 @@
+/*
+gridsquid.js
+
+osap tool drawing set
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+'use strict'
+
+import DT from '../client/interface/domTools.js'
+import { TS, EP } from '../core/ts.js'
+import PK from '../core/packets.js'
+
+import { Button } from '../interface/button.js'
+
+// probably rename this 
+export default function NetRunner(osap, xPlace, yPlace, poll) {
+  // ------------------------------------------------------ PLANE / ZOOM / PAN
+  let plane = $('<div>').addClass('plane').get(0)
+  let wrapper = $('#wrapper').get(0)
+  // odd, but w/o this, scaling the plane & the background together introduces some numerical errs,
+  // probably because the DOM is scaling a zero-size plane, or somesuch.
+  $(plane).css('background', 'url("/osapjs/client/interface/bg.png")').css('width', '100px').css('height', '100px')
+  let cs = 1 // current scale,
+  let dft = { s: cs, x: 0, y: 0, ox: 0, oy: 0 } // default transform
+
+  // zoom on wheel
+  wrapper.addEventListener('wheel', (evt) => {
+    if ($(evt.target).is('input, textarea')) return
+    evt.preventDefault()
+    evt.stopPropagation()
+
+    let ox = evt.clientX
+    let oy = evt.clientY
+
+    let ds
+    if (evt.deltaY > 0) {
+      ds = 0.025
+    } else {
+      ds = -0.025
+    }
+
+    let ct = DT.readTransform(plane)
+    ct.s *= 1 + ds
+    ct.x += (ct.x - ox) * ds
+    ct.y += (ct.y - oy) * ds
+
+    // max zoom pls thx
+    if (ct.s > 1.5) ct.s = 1.5
+    if (ct.s < 0.05) ct.s = 0.05
+    cs = ct.s
+    DT.writeTransform(plane, ct)
+    DT.writeBackgroundTransform(wrapper, ct)
+  })
+
+  // pan on drag,
+  wrapper.addEventListener('mousedown', (evt) => {
+    if ($(evt.target).is('input, textarea')) return
+    evt.preventDefault()
+    evt.stopPropagation()
+    DT.dragTool((drag) => {
+      drag.preventDefault()
+      drag.stopPropagation()
+      let ct = DT.readTransform(plane)
+      ct.x += drag.movementX
+      ct.y += drag.movementY
+      DT.writeTransform(plane, ct)
+      DT.writeBackgroundTransform(wrapper, ct)
+    })
+  })
+
+  // init w/ defaults,
+  DT.writeTransform(plane, dft)
+  DT.writeBackgroundTransform(wrapper, dft)
+
+  $(wrapper).append(plane)
+
+  // ------------------------------------------------------ RENDER / RERENDER
+  /*
+  some meta for this:
+    - should do absolute reckoning for nodes,
+    - vports attached to node positions, lines attached to vports,
+    - render(node) should redraw nodes & all downstream, then we can i.e. 
+        draw a node, then adjust it's height later, and redraw downstream 
+    ... or some other brilliant recurse-redraw situation 
+  */
+
+
+  // all nodes render into the plane, for now into the wrapper
+  // once ready to render, heights etc should be set,
+  let renderNode = (node) => {
+    let nel = $(`<div>`).addClass('node').get(0) // node class is position:absolute
+    nel.style.width = `${parseInt(node.width)}px`
+    nel.style.height = `${parseInt(node.height)}px`
+    nel.style.left = `${parseInt(node.pos.x)}px`
+    nel.style.top = `${parseInt(node.pos.y)}px`
+    // quick hack, 
+    if(node.vPorts[0].name == "ucbus drop"){
+      $(nel).append($(`<div>${node.routeTo.path[9]} ${node.name}</div>`).addClass('nodenamebus'))
+    } else {
+      $(nel).append($(`<div>${node.name}</div>`).addClass('nodename'))
+    }
+    if (node.el) $(node.el).remove()
+    node.el = nel
+    $(plane).append(node.el)
+  }
+
+  let DUPLEX_INCOMING = 0
+  let DUPLEX_OUTGOING = 1
+  let BUSHEAD_OUTGOING = 2
+  let BUSDROP_INCOMING = 3
+
+  let renderVPort = (vPort, type) => {
+    let nel = $('<div>').addClass('vPort').get(0)
+    nel.style.width = `${parseInt(vPort.parent.width) - 4}px`
+    let ph = perPortHeight - heightPadding
+    nel.style.height = `${parseInt(ph)}px`
+    nel.style.left = `${parseInt(vPort.parent.pos.x)}px`
+    let ptop = vPort.parent.pos.y + heightPadding + vPort.indice * perPortHeight + heightPadding * 0.5
+    nel.style.top = `${parseInt(ptop)}px`
+    $(nel).append($(`<div>${vPort.name}</div>`).addClass('vPortname'))
+    // draw outgoing svg, 
+    switch (type) {
+      case DUPLEX_INCOMING:
+        // no lines 
+        break;
+      case DUPLEX_OUTGOING: {
+        // anchor position (absolute, within attached-to), delx, dely
+        let line = DT.svgLine(perNodeWidth - 2, ph * 0.5, linkWidth, 0, 2)
+        $(nel).append(line) // appended, so that can be rm'd w/ the .remove() call
+        break;
+      }
+      case BUSHEAD_OUTGOING: {
+        // draw line, smaller 
+        let line = DT.svgLine(perNodeWidth - 2, ph * 0.5, linkWidth * 0.6, 0, 2)
+        $(nel).append(line)
+        break;
+      }
+      case BUSDROP_INCOMING:
+        // line, left
+        let line = DT.svgLine(0, ph * 0.5, -linkWidth * 0.6, 0, 2)
+        $(nel).append(line)
+        break;
+      default:
+        throw new Error("bad port type passed to rendervport")
+        break;
+    }
+    if (vPort.el) $(vPort.el).remove()
+    vPort.el = nel
+    $(plane).append(vPort.el)
+  }
+
+  // draw vals,
+  let perNodeWidth = 60
+  let linkWidth = 30
+  let perPortHeight = 120
+  let perDropOffset = 20
+  let heightPadding = 10
+
+  // for now, this'll look a lot like thar recursor,
+  // and we'll just do it once, assuming nice and easy trees ...
+  this.draw = (root) => {
+    console.log('draw')
+    let start = performance.now()
+    // rm all of these, redraw from scratch each time
+    $('.node').remove()
+    $('.vPort').remove()
+    $('.svgcont').remove()
+    // node-to-draw, vPort-entered-on, (and pos. of top of it's <el>), depth of recursion
+    let recursor = (node, entry, entryTop, depth) => {
+      node.width = perNodeWidth // time being, we are all this wide
+      node.height = heightPadding * 2 + node.vPorts.length * perPortHeight // 10px top / bottom, 50 per port
+      node.pos = {}
+      node.pos.x = depth * (perNodeWidth + linkWidth) + xPlace // our x-pos is related to our depth,
+      // and the 'top' - now, if entry has an .el / etc data - if ports have this data as well, less calc. here
+      if (entry) {
+        node.pos.y = entryTop - entry.indice * perPortHeight - heightPadding
+      } else {
+        node.pos.y = yPlace + 160
+      }
+      // draw ready?
+      renderNode(node)
+      // traverse,
+      for (let vp of node.vPorts) {
+        if (!vp) continue
+        switch (vp.portTypeKey) {
+          case EP.PORTTYPEKEY.DUPLEX:
+            if (vp == entry) {  // this is the vport we arrived on,
+              renderVPort(vp, DUPLEX_INCOMING)
+            } else if (vp.reciprocal[0]) { // there is a node across this bridge 
+              renderVPort(vp, DUPLEX_OUTGOING)
+              recursor(vp.reciprocal[0].parent, vp.reciprocal[0], node.pos.y + heightPadding + vp.indice * perPortHeight, depth + 1)
+            } else {
+              renderVPort(vp, DUPLEX_INCOMING)
+            }
+            break;
+          case EP.PORTTYPEKEY.BUSHEAD:
+            // it's definitely not the entry port, 
+            renderVPort(vp, BUSHEAD_OUTGOING)
+            // start rendering the line, then walk *down* it, yeah? 
+            // inrementing by the height of each reciprocal-parent, recursing thru here.. 
+            let dropOffset = 0
+            let ax = node.width + linkWidth * 0.5 // anchor for 'bus' line, relative vport bottom left corner 
+            let ay = parseInt(vp.el.style.height) * 0.5 - perDropOffset * 0.5
+            for (let d = 0; d < vp.maxAddresses; d++) {
+              if (vp.reciprocal[d]) {
+                console.warn(`found bus recip ${d}`)
+                // this will break when busses have anything on the other end, or bus drops have more than one vport 
+                dropOffset += parseInt(vp.el.style.height) * 0.5 + 10
+                recursor(vp.reciprocal[d].parent, vp.reciprocal[d], node.pos.y + heightPadding + vp.indice * perPortHeight + dropOffset, depth + 1)
+                // now that recursor has drawn downstream node, we can get its height 
+                dropOffset += parseInt(vp.el.style.height) * 0.5 + perDropOffset + 10
+              } else {
+                let line = DT.svgLine(node.width + linkWidth * 0.5 - 5, parseInt(vp.el.style.height) * 0.5 + dropOffset, 10, 0, 2)
+                $(vp.el).append(line)
+                dropOffset += perDropOffset
+              }
+            }
+            let busLine = DT.svgLine(ax, ay, 0, dropOffset, 2)
+            $(vp.el).append(busLine)
+            break;
+          case EP.PORTTYPEKEY.BUSDROP:
+            // bus drop, simple ?
+            renderVPort(vp, BUSDROP_INCOMING)
+            break;
+          default:
+            console.warn('vport rep w/ bad type key')
+            return;
+        }
+      } // end for-vports 
+    } // end recursor def, 
+    // start recursor 
+    try {
+      recursor(root, null, 0, 0)
+    } catch (err) {
+      console.error(err)
+    }
+    console.warn(`draw was ${performance.now() - start}ms`)
+  } // end draw 
+
+  // ------------------------------------------------------ SWEEP ROUTINES
+
+  // have a route, last neighbour was node, departure port was p, 
+  // arrival addr was d, nextRoute is full path to this node 
+  let sweepForVPorts = async (node, p, d, nextRoute) => {
+    // get num vPorts at next node, and use route back to discover exit port
+    let nodeRes = await osap.read(nextRoute, 'name', 'numVPorts')
+    //console.warn('noderes route', nodeRes.route)
+    // if this works, a node exists on the other side of this port,
+    let nextNode = {
+      routeTo: nextRoute,
+      name: nodeRes.data.name,
+      vPorts: []
+    }
+    // the port that our queries enter on: 
+    let entryPort = await osap.readEntryPort(nextRoute)
+    // for each next in line,
+    for (let np = 0; np < nodeRes.data.numVPorts; np++) {
+      try {
+        // 1st, get basics & assemble an object,
+        let portRes = await osap.read(nextRoute, 'vport', np, 'name', 'portTypeKey', 'maxSegLength', 'maxAddresses')
+        let vPort = {
+          parent: nextNode,
+          indice: parseInt(np),
+          name: portRes.data.name,
+          portTypeKey: portRes.data.portTypeKey,
+          maxSegLength: portRes.data.maxSegLength,
+          maxAddresses: portRes.data.maxAddresses,
+          reciprocal: new Array(portRes.data.maxAddresses), // empty array... 
+          portStatus: new Array(portRes.data.maxAddresses), // also empty
+        }
+        vPort.reciprocal.fill(false)
+        vPort.portStatus.fill(false)
+        nextNode.vPorts.push(vPort)
+        // if this was the port we entered on, hook it up, so that we can walk links 
+        if (np == entryPort) {
+          console.warn('set r')
+          node.vPorts[p].reciprocal[d] = nextNode.vPorts[np]  // p (node-port) np (next-node-port)
+          nextNode.vPorts[np].reciprocal[0] = node.vPorts[p]  // since we are not traversing up busses, [0]
+        }
+        // now we want to know status(es) for the port: is it open?
+        for (let d = 0; d < nextNode.vPorts[np].maxAddresses; d++) {
+          let stat = await osap.read(nextRoute, 'vport', np, 'portStatus', d)
+          vPort.portStatus[d] = stat.data.portStatus
+        }
+        // 
+      } catch (err) {
+        console.error(err)
+        console.error('sweep / draw error at port', p, ',', nextNode.name)
+        nextNode.vPorts.push(null)
+      }
+    } // close query on next ports,
+    // continue
+    await sweepRecurse(nextNode)
+  }
+
+  // node is an element in the object tree, representing an osap node. 
+  // it has vports, w/ names, etc. we build this object recursively, 
+  /*
+  let node = {
+    name: "str",
+    routeTo: uint8Array,
+    vPorts: [
+      {
+        indice: 0,
+        name: "str",
+        portTypeKey: key,                 // bus_head, bus_drop, duplex
+        maxSegLength: <num>,              // max size of a segment transmitted here 
+        portStatus: array[maxAddresses],  // open / closed / opening / closing, 
+        maxAddresses: <num>,              // count of drops on other end, 
+        parent: <node>,                   // 
+        reciprocal: array[<vPort>]        // linked drops, 
+      }
+    ]
+  }
+  */
+  // at this point, the node object is loaded with node.vPorts, 
+  // this generates new routes to dive down / 'across' each vport, hopefully finding another node there 
+  let sweepRecurse = async (node) => {
+    for (let p in node.vPorts) {
+      // traversal is different for each type:
+      if (node.vPorts[p].portTypeKey == EP.PORTTYPEKEY.DUPLEX) {
+        // don't go back up:
+        if (node.vPorts[p].reciprocal[0]) continue
+        // don't try closed ports,
+        if (node.vPorts[p].portStatus[0] != EP.PORTSTATUS.OPEN) {
+          if (!node.isRoot) {
+            try {
+              console.warn(`req open ${p} at ${node.routeTo.path}`)
+              await osap.write(node.routeTo, 'vport', parseInt(p), 'portStatus', 0, true)
+            } catch (err) {
+              console.error(err)
+            }
+          } else {
+            // closed vport at the root: could do direct ask, at the moment the remote
+            // proc. dies when the wss connection dies, so this would be futile 
+          }
+          continue; // continue past closed ports 
+        } // end closed-port term, 
+        // ok, we're set to dive, add a fwding term to the path, 
+        let nextRoute = {
+          path: new Uint8Array(node.routeTo.path.length + 3),
+          segsize: 128
+        }
+        nextRoute.path.set(node.routeTo.path)
+        nextRoute.path[node.routeTo.path.length] = PK.PORTF.KEY
+        TS.write('uint16', parseInt(p), nextRoute.path, node.routeTo.path.length + 1, true)
+        //console.log('next path', nextRoute)
+        await sweepForVPorts(node, p, 0, nextRoute)
+      } else if (node.vPorts[p].portTypeKey == EP.PORTTYPEKEY.BUSHEAD) {
+        // try each potential drop, don't try self (0 addr)
+        for (let d = 1; d < node.vPorts[p].maxAddresses; d++) {
+          // don't need to check against walking back upstream, that would be a bus-drop returning,
+          // will only need to do that on multi-host busses, 
+          // not closed ports, and don't bother trying to re-open: 
+          // at the moment ucbus does this automatically
+          if (node.vPorts[p].portStatus[d] != EP.PORTSTATUS.OPEN) continue;
+          console.warn(`continue on drop ${d}`)
+          // ok, ready to dive down:
+          let nextRoute = {
+            path: new Uint8Array(node.routeTo.path.length + 5),
+            segsize: 128
+          }
+          nextRoute.path.set(node.routeTo.path) // write in old path, 
+          nextRoute.path[node.routeTo.path.length] = PK.BUSF.KEY  // bus forward move 
+          TS.write('uint16', parseInt(p), nextRoute.path, node.routeTo.path.length + 1, true) // from this vp
+          TS.write('uint16', parseInt(d), nextRoute.path, node.routeTo.path.length + 3, true) // to this rxaddr
+          await sweepForVPorts(node, p, d, nextRoute)
+        }
+      } else if (node.vPorts[p].portTypeKey == EP.PORTTYPEKEY.BUSDROP) {
+        // won't try to scan back *up* the bus, but might, if i.e. we want to scope-in
+        // on usb connection to i.e. a motor (for debug) while a system is running... 
+        console.warn(`bus drop ${p}`)
+        return
+      } else {
+        throw new Error("bad vport typekey")
+      }
+    } // end loop over vports
+  }
+
+  let sweeper = async () => {
+    // start from nil,
+    let root = {} // home node,
+    root.vPorts = [] // our ports,
+    root.name = osap.name
+    root.isRoot = true
+    root.routeTo = {
+      path: new Uint8Array(0),
+      segsize: 128
+    }
+    // make definitions of our local ports: this we do w/o querying, 
+    for (let p in osap.vPorts) {
+      let pOut = {
+        indice: parseInt(p),
+        name: osap.vPorts[p].name,
+        portTypeKey: osap.vPorts[p].portTypeKey,
+        maxSegLength: osap.vPorts[p].maxSegLength,
+        maxAddresses: 1,
+        reciprocal: [],
+        portStatus: [osap.vPorts[p].status()],    // port status(es) - always an array, for busses 
+      }
+      root.vPorts.push(pOut)
+      pOut.parent = root
+      // kick closed ports: this is different then the remainder of recurse, because we have 
+      // direct access to it, 
+      if (pOut.portStatus[0] == EP.PORTSTATUS.CLOSED) {
+        osap.vPorts[p].requestOpen()
+      }
+    }
+    // now we can start here, to recurse through
+    try {
+      await sweepRecurse(root)
+    } catch (err) {
+      console.error('err during sweep', err)
+    }
+    // return the structure
+    return root
+  }
+
+  // hmm ...
+  let depthAnalysis = (root) => {
+    let depths = [0]
+    let recursor = (vPort, depth) => {
+      if (depth > 6) return // depth limit
+      if (vPort.reciprocal) { // places to go,
+        for (let vp of vPort.reciprocal.parent.vPorts) {
+          depths.push(depth + 1)
+          console.log(vPort.reciprocal.parent.name)
+          if (vp == vPort.reciprocal) continue // skip entry // TODO circular graphs would stil f us here
+          recursor(vp, depth + 1)
+        }
+      }
+    } // end recursor,
+    for (let vp of root.vPorts) {
+      recursor(vp, 0)
+    }
+    return Math.max(...depths)
+  }
+
+  // ------------------------------------------------------ Sweep Startup / Loop  
+
+  let setPollingStatus = (val) => {
+    if (val) {
+      runSweepRoutine()
+    } else {
+      if (ctrl.timer) {
+        clearTimeout(ctrl.timer)
+        ctrl.timer = undefined
+      }
+    }
+  }
+
+  // TODO: cleanup, sweep should definitely return as a promise, 
+  // can set button state with / try or try-not to draw the results based on that 
+
+  let BTN_RED = 'rgb(242, 201, 201)'
+  let BTN_GRN = 'rgb(201, 242, 201)'
+  let BTN_YLW = 'rgb(240, 240, 180)'
+  let BTN_GREY = 'rgb(242, 242, 242)'
+
+  let runSweepRoutine = async () => {
+    if (ctrl.awaiting) return
+    ctrl.awaiting = true
+    $(pollBtn).css('background-color', BTN_YLW)
+    try {
+      let res = await sweeper()
+      this.draw(res)
+      $(pollBtn).css('background-color', BTN_GREY)
+    } catch (err) {
+      console.error('sweeper err', err)
+      $(pollBtn).css('background-color', BTN_RED)
+    }
+    ctrl.awaiting = false
+    if (ctrl.polling) {
+      ctrl.timer = setTimeout(runSweepRoutine, ctrl.interval)
+    }
+  }
+
+  // ------------------------------------------------------ Poll Control 
+
+  let ctrl = {
+    polling: poll, // init w/ startup arg 
+    awaiting: false,
+    interval: 1000,
+    timer: undefined
+  }
+
+  let titleBtn = new Button(xPlace, yPlace, 94, 14, 'netrunner ->')
+
+  let pollBtn = new Button(xPlace, yPlace + 30, 54, 14, 'poll')
+  pollBtn.onClick(() => {
+    runSweepRoutine()
+    pollBtn.good('ok', 250)
+  })
+
+  let loopBtn = new Button(xPlace, yPlace + 60, 54, 14, 'loop')
+  loopBtn.onClick(() => {
+    if(ctrl.polling) {
+      ctrl.polling = false 
+    } else {
+      ctrl.polling = true 
+    }
+    setTimeout(setPollingState, 0)
+  })
+
+  let setPollingState = () => {
+    if (ctrl.polling) {
+      $(loopBtn.elem).text('stop')
+      $(loopBtn.elem).css('background-color', BTN_GRN)
+      runSweepRoutine()
+    } else {
+      $(loopBtn.elem).text('loop')
+      $(loopBtn.elem).css('background-color', BTN_GREY)
+    }
+  }
+
+  // ------------------------------------------------------ START CONDITION 
+
+  setPollingState()
+}
diff --git a/system/javascript/osapjs/test/circleGen.js b/system/javascript/osapjs/test/circleGen.js
new file mode 100644
index 0000000000000000000000000000000000000000..5f41c2e72ed8c81b0f92896f791c00787ea8256e
--- /dev/null
+++ b/system/javascript/osapjs/test/circleGen.js
@@ -0,0 +1,37 @@
+/*
+circleGen.js
+
+makes circular toolpaths, for testy testy 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// aye it's space filling curves basically innit, 
+export default function circleGen(origin, radius, segmentLength){
+  let path = [] 
+  let x = 0 
+  let y = 0 
+  let z = 0
+  // how long (in rads) will each segment be?
+  let radLength = segmentLength / radius 
+  console.log(`arcLen ${segmentLength} in rads is ${radLength}`)
+  let rads = 0 
+  while(rads < 2 * Math.PI){
+    path.push([origin[0] + Math.cos(rads) * radius, origin[1] + Math.sin(rads) * radius, origin[2]])
+    rads += radLength
+  }
+  return path
+}
+
+// should add to this thing, do `gennies.js`
+let getCircleCoordinates = (theta, scalar) => {
+  let tc = [Math.sin(theta) * scalar, Math.cos(theta) * scalar, 0, 0]
+  console.log(tc)
+  return tc
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/test/snakeGen.js b/system/javascript/osapjs/test/snakeGen.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9ad5a5151a2225b4ace0941a6652b26a978d42f
--- /dev/null
+++ b/system/javascript/osapjs/test/snakeGen.js
@@ -0,0 +1,61 @@
+/*
+snakGen.js
+
+makes snake toolpaths, probably to test 3d printers
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// aye it's space filling curves basically innit, 
+// **e values are in CUBIC MM !!!** 
+export default function snakeGen(args) {
+  // args.width, args.depth, args.height, args.trackWidth, args.trackHeight, args.segmentLength
+  let path = []
+  let x = 0
+  let y = 0
+  let z = 0
+  let e = 0 // E units are in cubic mm ! 
+  while (z < args.height) {
+    z += args.trackHeight
+    while (y < args.depth) {
+      while (x < args.width) {
+        path.push([x, y, z, e])
+        x += args.segmentLength
+        e += args.trackWidth * args.trackHeight * args.segmentLength // cubic units,
+      }
+      y += args.trackWidth
+      e += args.trackWidth * args.trackHeight * args.trackWidth
+      while (x > 0) {
+        path.push([x, y, z, e])
+        x -= args.segmentLength
+        e += args.trackWidth * args.trackHeight * args.segmentLength // cubic units,
+      }
+      y += args.trackWidth
+      // since we are going *up* by trackwidth, trackwidth == segmentLength 
+      e += args.trackWidth * args.trackHeight * args.trackWidth
+    }
+    x = 0
+    y = 0
+  }
+  // do the rate... 
+  // cubic mm per linear mm = 
+  let cubicMMperLinearMM = args.trackWidth * args.trackHeight
+  let linearRate = args.flowRate / cubicMMperLinearMM
+  for (let p in path) {
+    path[p] = { target: path[p], rate: linearRate }
+  }
+  return path
+}
+
+// should add to this thing, do `gennies.js`
+let getCircleCoordinates = (theta, scalar) => {
+  let tc = [Math.sin(theta) * scalar, Math.cos(theta) * scalar, 0, 0]
+  console.log(tc)
+  return tc
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/utes/cobs.js b/system/javascript/osapjs/utes/cobs.js
new file mode 100644
index 0000000000000000000000000000000000000000..af2f7bce399d4371561cb3d89b056d9f8e68a2f5
--- /dev/null
+++ b/system/javascript/osapjs/utes/cobs.js
@@ -0,0 +1,67 @@
+/*
+cobs.js
+
+consistent overhead byte stuffing, wikipedia implementation
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+function cobsEncode(buf) {
+  var dest = [0];
+  // vfpt starts @ 1,
+  var code_ptr = 0;
+  var code = 0x01;
+
+  function finish(incllast) {
+    dest[code_ptr] = code;
+    code_ptr = dest.length;
+    incllast !== false && dest.push(0x00);
+    code = 0x01;
+  }
+
+  for (var i = 0; i < buf.length; i++) {
+    if (buf[i] == 0) {
+      finish();
+    } else {
+      dest.push(buf[i]);
+      code += 1;
+      if (code == 0xFF) {
+        finish();
+      }
+    }
+  }
+  finish(false);
+
+  // close w/ zero
+  dest.push(0x00)
+
+  return Uint8Array.from(dest);
+}
+
+// COBS decode, tailing zero, that was used to delineate this buffer,
+// is assumed to already be chopped, thus the end is the end 
+
+function cobsDecode(buf) {
+  var dest = [];
+  for (var i = 0; i < buf.length;) {
+    var code = buf[i++];
+    for (var j = 1; j < code; j++) {
+      dest.push(buf[i++]);
+    }
+    if (code < 0xFF && i < buf.length) {
+      dest.push(0);
+    }
+  }
+  return Uint8Array.from(dest)
+}
+
+export default {
+  encode: cobsEncode,
+  decode: cobsDecode
+}
diff --git a/system/javascript/osapjs/utes/diff.js b/system/javascript/osapjs/utes/diff.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad0c94aec51fedebce571d8dfa3a906a0096a6ce
--- /dev/null
+++ b/system/javascript/osapjs/utes/diff.js
@@ -0,0 +1,29 @@
+/*
+diff.js
+
+js object diffing utes
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2020
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+let settingsDiff = (a, b, name) => {
+  for(let ka in a){ // for key of a in a,
+    let match = false 
+    for(let kb in b){ // for key of b in b,
+      if(ka == kb){   // if we have matching keys, OK 
+        match = true 
+      }
+    }
+    if(!match) throw new Error(`key '${ka}' in ${name} has no match in provided settings`)
+  }
+}
+
+export {
+  settingsDiff
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vcs/motionHead.js b/system/javascript/osapjs/vcs/motionHead.js
new file mode 100644
index 0000000000000000000000000000000000000000..04a3ed30cc07ab79f2de29530ccb78578203ea5b
--- /dev/null
+++ b/system/javascript/osapjs/vcs/motionHead.js
@@ -0,0 +1,36 @@
+/*
+motionHead.js
+
+motion-head firmware mirror 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import PK from "../core/packets.js"
+import AXLMotionVM from "../vms/axlMotionVM.js"
+import VC from "./vc.js"
+
+export default function MotionHead(osap, _settings) {
+  this.vc = new VC(osap, "rt_motion-head")
+  this.setup = async () => {
+    try {
+      await this.vc.setup()
+      console.log(`found motion-head fw, now loading settings`)
+      console.warn(`haven't written settings-diff yet...`)
+      this.motion = new AXLMotionVM(osap, this.vc.route, _settings.accelLimits.length)
+      this.motion.settings = _settings
+      await this.motion.setup()
+      console.log(`motion-head setup OK`)
+    } catch (err) {
+      console.error(err)
+      throw err 
+    }
+  }
+  // 
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vcs/vc.js b/system/javascript/osapjs/vcs/vc.js
new file mode 100644
index 0000000000000000000000000000000000000000..2fd19cfe7a042d75b0c1b18a877d712f76ce0847
--- /dev/null
+++ b/system/javascript/osapjs/vcs/vc.js
@@ -0,0 +1,27 @@
+/*
+vs.js
+
+virtual context base class, 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+export default function VC(osap, name) {
+  // setup is async,
+  this.setup = async () => {
+    // find self via name & netRunner lookup,
+    try {
+      let vvt = await osap.nr.find(name)
+      this.route = vvt.route
+    } catch (err) {
+      console.error(`failed to setup the VC with name ${name}`)
+      console.error(err)
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/axlActuator.js b/system/javascript/osapjs/vms/axlActuator.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba9988cf4e3ee97d6800e6574a0dd2fc6ab82d81
--- /dev/null
+++ b/system/javascript/osapjs/vms/axlActuator.js
@@ -0,0 +1,219 @@
+/*
+axlActuator.js
+
+axl motion controller actuator model 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) and AXL projects 
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import PK from '../core/packets.js'
+import { TS, VT } from '../core/ts.js'
+import TIME from '../core/time.js'
+
+import { settingsDiff } from '../utes/diff.js'
+
+export default function AXLActuator(osap, route, _settings) {
+  // default settings, for now assuming all stepper motors, can bust that up later 
+  this.settings = {
+    name: "rt_unknownActuator",
+    accelLimits: [100, 100, 100],
+    velocityLimits: [100, 100, 100],
+    queueStartDelay: 500,
+    actuatorID: 0,
+    axis: 0,            // this & below are ~ motor-type specific, which we can bust out later 
+    invert: false,
+    microstep: 4,
+    spu: 20,
+    cscale: 0.25,
+  }
+  // diff for extra or missing keys 
+  if (_settings) {
+    settingsDiff(this.settings, _settings, "AXLActuator")
+    this.settings = JSON.parse(JSON.stringify(_settings))
+  }
+  // count DOF 
+  let numDof = this.settings.accelLimits.length
+
+  let axlSettingsEP = osap.endpoint("axlSettingsMirror")
+  axlSettingsEP.addRoute(PK.route(route).sib(2).end())
+
+  this.setupAxl = async () => {
+    try {
+      let datagram = new Uint8Array(numDof * 4 * 2 + 4 + 1)
+      let wptr = 0
+      for (let a = 0; a < numDof; a++) {
+        wptr += TS.write("float32", this.settings.accelLimits[a], datagram, wptr)
+        wptr += TS.write("float32", this.settings.velocityLimits[a], datagram, wptr)
+      }
+      wptr += TS.write("uint32", this.settings.queueStartDelay, datagram, wptr)
+      wptr += TS.write("uint8", this.settings.actuatorID, datagram, wptr)
+      await axlSettingsEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // we could do a states-ep write w/ a mirror here, but normally do this with a broadcast anyways... 
+
+  let motorSettingsEP = osap.endpoint("motorSettingsMirror")
+  motorSettingsEP.addRoute(PK.route(route).sib(9).end())
+
+  this.setupMotor = async () => {
+    try {
+      let datagram = new Uint8Array(12)
+      TS.write('uint8', this.settings.axis, datagram, 0)
+      TS.write('boolean', this.settings.invert, datagram, 1)
+      TS.write('uint16', this.settings.microstep, datagram, 2)
+      TS.write('float32', this.settings.spu, datagram, 4)
+      TS.write('float32', this.settings.cscale, datagram, 8)
+      await motorSettingsEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // write state requests direct to this motor, 
+  let stateRequestsOutEP = osap.endpoint("motorStateMirror")
+  stateRequestsOutEP.addRoute(PK.route(route).sib(3).end())
+
+  let AXL_MODE_ACCEL = 1
+  let AXL_MODE_VELOCITY = 2
+  let AXL_MODE_POSITION = 3
+  let AXL_MODE_QUEUE = 4
+
+  this.writeStateBroadcast = async (vals, mode, set) => {
+    try {
+      if(vals.length != numDof) throw new Error(`state-write request with ${vals.length} DOF, actuator should have ${numDof}`)
+      // pack 'em up, 
+      let datagram = new Uint8Array(numDof * 4 + 2)
+      let wptr = 0
+      datagram[wptr++] = mode 
+      datagram[wptr++] = set  
+      for (let a = 0; a < numDof; a++) {
+        wptr += TS.write("float32", vals[a], datagram, wptr)
+      }
+      // and send it along on our broadcast channel, 
+      await stateRequestsOutEP.write(datagram, "ackless")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.gotoVelocity = async (vel) => {
+    try {
+      // vel = this.cartesianToActuatorTransform(vel)
+      await this.writeStateBroadcast(vel, AXL_MODE_VELOCITY, false)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  let motionStateQuery = null
+  this.awaitMotionEnd = async () => {
+    try {
+      if (!motionStateQuery) throw new Error("on awaitMotionEnd, query isn't yet setup")
+      return new Promise((resolve, reject) => {
+        let check = async () => {
+          let state = await motionStateQuery.pull()
+          // console.log(state)
+          if (state[0] == 0) {
+            resolve()
+          } else {
+            setTimeout(check, 0)
+          }
+        }
+        check()
+      })
+    } catch (err) {
+      throw err
+    }
+  }
+
+  let limitStateQuery = null 
+  this.getLimitState = async () => {
+    try {
+      if (!limitStateQuery) throw new Error("on getLimitState, query isn't yet setup")
+      let data = await limitStateQuery.pull()
+      return (data[0] > 0)
+    } catch (err) {
+      throw err 
+    }
+  }
+
+  let stateQuery = null 
+    this.getStates = async () => {
+    try {
+      if(!stateQuery) throw new Error("on getState, query isn't yet setup")
+      let data = await stateQuery.pull()
+      let rptr = 0 
+      let state = {
+        positions: [],
+        velocities: [],
+        accelerations: [],
+        target: []
+      }
+      for(let a = 0; a < numDof; a ++){
+        state.positions.push(TS.read('float32', data, rptr + 0))
+        state.velocities.push(TS.read('float32', data, rptr + 4))
+        state.accelerations.push(TS.read('float32', data, rptr + 8))
+        state.target.push(TS.read('float32', data, rptr + 12))
+        rptr += 16
+      }
+      state.segDistance = TS.read('float32', data, rptr += 4)
+      state.segVel = TS.read('float32', data, rptr += 4)
+      state.segAccel = TS.read('float32', data, rptr += 4)
+      state.mode = data[rptr ++]
+      state.haltState = data[rptr ++]
+      state.queueState = data[rptr ++]
+      state.headIndice = data[rptr ++]
+      state.tailIndice = data[rptr ++]
+      return state 
+      /*
+        // vect_t's 
+        for(uint8_t a = 0; a < AXL_NUM_DOF; a ++){
+          ts_writeFloat32(state.positions.axis[a], data, &wptr);
+          ts_writeFloat32(state.velocities.axis[a], data, &wptr);
+          ts_writeFloat32(state.accelerations.axis[a], data, &wptr);
+          ts_writeFloat32(state.target.axis[a], data, &wptr);
+        }
+        // inter-segment state, 
+        ts_writeFloat32(state.segDistance, data, &wptr);
+        ts_writeFloat32(state.segVel, data, &wptr);
+        ts_writeFloat32(state.segAccel, data, &wptr);
+        // mode, halt state, queue state, and queue pointer positions 
+        ts_writeUint8(state.mode, data, &wptr);
+        ts_writeUint8(state.haltState, data, &wptr);
+        ts_writeUint8(state.queueState, data, &wptr);
+        ts_writeUint8(state.head->indice, data, &wptr);
+        ts_writeUint16(state.tail->indice, data, &wptr);
+      */
+    } catch (err) {
+      throw err 
+    }
+  }
+
+  this.setup = async (graph) => {
+    try {
+      if(!graph) graph = await osap.nr.sweep()
+      // find the motion state endpoint... 
+      let motionStateVVT = await osap.nr.findWithin("ep_motionState", this.settings.name, graph)
+      motionStateQuery = osap.query(PK.VC2EPRoute(motionStateVVT.route))
+      // and the limit state endpoint... 
+      let limitStateVVT = await osap.nr.findWithin("ep_limitSwitchState", this.settings.name, graph)
+      limitStateQuery = osap.query(PK.VC2EPRoute(limitStateVVT.route))
+      // and the *state* endpoint... 
+      let stateVVT = await osap.nr.findWithin("ep_axlState", this.settings.name, graph)
+      stateQuery = osap.query(PK.VC2EPRoute(stateVVT.route))
+      await this.setupAxl()
+      await this.setupMotor()
+    } catch (err) {
+      throw err
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/axlCore.js b/system/javascript/osapjs/vms/axlCore.js
new file mode 100644
index 0000000000000000000000000000000000000000..8b7d8db44d6b0aa742c67c931c6614b1258d9747
--- /dev/null
+++ b/system/javascript/osapjs/vms/axlCore.js
@@ -0,0 +1,779 @@
+/*
+axlCore.js
+
+axl motion controller central node 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) and AXL projects 
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import PK from '../core/packets.js'
+import { TS, EP } from '../core/ts.js'
+import TIME from '../core/time.js'
+
+import { settingsDiff } from '../utes/diff.js'
+
+import AXLActuator from './axlActuator.js'
+
+let numDof = 0
+
+// settings... an obj, and _actuators, a list of names & axis-maps... 
+export default function AXLCore(osap, _settings, _actuators) {
+  // actuators should be... actuator objects, as in axlActuator.js 
+  numDof = _settings.bounds.length
+  console.warn(`AXL with ${numDof} DOF...`)
+
+  // we have settings that we... diff against these defaults, 
+  // defaults, 
+  this.settings = {
+    junctionDeviation: 0.05,
+    queueStartDelay: 500,
+    bounds: [100, 100, 100],
+    accelLimits: [100, 100, 100],
+    velocityLimits: [100, 100, 100]
+  }
+
+  // if present, check against 
+  if (_settings) {
+    settingsDiff(this.settings, _settings, "AXLCore")
+    this.settings = JSON.parse(JSON.stringify(_settings))
+  }
+
+  // we do a little spu-per-second guarding, 
+  let maxTickPerSecond = 10000
+  for (let actu of _actuators) {
+    let maxRate = maxTickPerSecond / actu.spu
+    let setVMax = this.settings.velocityLimits[actu.axis]
+    if (setVMax > maxRate) {
+      throw new Error(`given maximum ticks / second of ${maxTickPerSecond}, ${actu.name} velocity limit should be below ${maxRate} but is set to ${setVMax}`)
+    } else {
+      console.warn(`${actu.name} has tick-limit allowable maxRate of ${maxRate}, is set to ${setVMax} vMax`)
+    }
+  }
+
+  // internal offset stash... 
+  let positionOffset = [0, 0, 0, 0]
+  let parkPosition = [0, 0, 0, 0]
+
+  // we have some transforms, 
+  this.cartesianToActuatorTransform = (vals, position = false) => {
+    vals = JSON.parse(JSON.stringify(vals))
+    if (position) {
+      for (let a = 0; a < numDof; a++) {
+        vals[a] -= positionOffset[a]
+      }
+    }
+    // do the transform... 
+    let tfVals = new Array(numDof)
+    // tfVals[0] = 0.5 * (vals[0] + vals[1])
+    // tfVals[1] = 0.5 * (vals[0] - vals[1])
+    tfVals[0] = vals[0] + vals[1]
+    tfVals[1] = vals[0] - vals[1]
+    tfVals[2] = vals[2]
+    // return the new, 
+    return tfVals
+  }
+
+  this.actuatorToCartesianTransform = (vals, position = false) => {
+    // inverts the above transform 
+    let tfVals = new Array(numDof)
+    tfVals[0] = 0.5 * (vals[0] + vals[1])
+    tfVals[1] = 0.5 * (vals[0] - vals[1])
+    tfVals[2] = vals[2]
+    // hmmm 
+    if (position) {
+      for (let a = 0; a < numDof; a++) {
+        // add back offset, 
+        tfVals[a] += positionOffset[a]
+      }
+    }
+    // return new 
+    return tfVals
+  }
+
+  // we have actuators, which we'll fill in on setup, 
+  let actuators = []
+  this.actuators = actuators
+  this.spindleVM = {}
+
+  // ------------------------------------------------------ Plumbing
+
+  this.setup = async (graph) => {
+    // first we want a graph... 
+    try {
+      // ---------------------------------------- Get a Graph Object 
+      console.log(`SETUP: collecting a graph...`)
+      if (!graph) graph = await osap.nr.sweep()
+      // ---------------------------------------- Find and build Actuators 
+      for (let actu of _actuators) {
+        console.log(`SETUP: looking for ${actu.name}...`)
+        let vvt = await osap.nr.find(actu.name, graph)
+        // these have settings, some of which are inherited from us, others from the list... 
+        let actuSettings = {
+          name: actu.name,
+          accelLimits: JSON.parse(JSON.stringify((this.settings.accelLimits))),
+          velocityLimits: JSON.parse(JSON.stringify((this.settings.velocityLimits))),
+          queueStartDelay: this.settings.queueStartDelay,
+          actuatorID: actuators.length,
+          axis: actu.axis,
+          invert: actu.invert,
+          microstep: actu.microstep,
+          spu: actu.spu,
+          cscale: actu.cscale
+        }
+        actuators.push(new AXLActuator(osap, PK.VC2VMRoute(vvt.route), actuSettings))
+        console.log(`SETUP: found and built ${actu.name}...`)
+      }
+      // then set 'em up, 
+      for (let actu of actuators) {
+        console.warn(`SETUP: initializing ${actu.settings.name}... is num ${actu.settings.actuatorID}`)
+        await actu.setup(graph)
+        console.log(`SETUP: init ${actu.settings.name} OK`)
+      }
+      // throw new Error('halt')
+      // we want a string-list of our actuator names, 
+      let actuatorNames = []
+      for (let actu of actuators) {
+        actuatorNames.push(actu.settings.name)
+      }
+      // ---------------------------------------- Plumb planned moves -> queue ingestion 
+      console.log(`SETUP: building a broadcast route for planned moves...`)
+      let plannedMoveChannel = await osap.hl.buildBroadcastRoute("ep_segmentsOut", actuatorNames, "ep_segmentsIn", false, graph)
+      console.log(`SETUP: broadcast route for planned moves on ${plannedMoveChannel} OK`)
+      // ---------------------------------------- Plumb state requests from us -> actuators 
+      console.log(`SETUP: building a broadcast route for state-request moves...`)
+      let stateRequestChannel = await osap.hl.buildBroadcastRoute("ep_stateRequestsOut", actuatorNames, "ep_axlState", false, graph)
+      console.log(`SETUP: broadcast route for state-request moves on ${stateRequestChannel} OK`)
+      // ---------------------------------------- Plumb halt signals from us down to actuators 
+      console.log(`SETUP: building a broadcast route for halt signals...`)
+      let haltChannel = await osap.hl.buildBroadcastRoute("ep_haltOutJS", actuatorNames, "ep_haltIn", false, graph)
+      console.log(`SETUP: broadcast route for planned moves on ${haltChannel} OK`)
+      // ---------------------------------------- Plumb halt signals from actuators back to us, 
+      console.log(`SETUP: linking remote halts back to us...`)
+      let haltInVVT = await osap.nr.find("ep_haltInJS", graph)
+      for (let actu of actuators) {
+        let haltOutVVT = await osap.nr.findWithin("ep_haltOut", actu.settings.name, graph)
+        let haltConnectRoute = await osap.nr.findRoute(haltOutVVT, haltInVVT)
+        // this should be high(er) priority than queue acks... set time-to-live low-ish 
+        haltConnectRoute.ttl = 500
+        haltConnectRoute.mode = EP.ROUTEMODE_ACKLESS
+        await osap.mvc.setEndpointRoute(haltOutVVT.route, haltConnectRoute)
+        console.log(`SETUP: connected ${actu.settings.name} haltOut to JS`)
+      }
+      console.warn(`SETUP: TODO: link remote halts to the broadcast as well !`)
+      // ---------------------------------------- Plumb actuator queue-acks to us... 
+      console.log(`SETUP: linking remote segmentAck signals back to us...`)
+      let segmentAckInVVT = await osap.nr.find("ep_segmentAckIn", graph)
+      for (let actu of actuators) {
+        let segmentAckOutVVT = await osap.nr.findWithin("ep_segmentAckOut", actu.settings.name, graph)
+        let connectRoute = await osap.nr.findRoute(segmentAckOutVVT, segmentAckInVVT)
+        // lower priority than halt signals, but higher than general purpose 
+        connectRoute.ttl = 1000
+        connectRoute.mode = EP.ROUTEMODE_ACKLESS
+        await osap.mvc.setEndpointRoute(segmentAckOutVVT.route, connectRoute)
+        console.log(`SETUP: connected ${actu.settings.name} segmentAck to JS`)
+      }
+      console.log(`SETUP: queue signals are piped...`)
+      // ---------------------------------------- Plumb actuator queue-move-complete to us...
+      console.log(`SETUP: linking remote segmentComplete signals back to us...`)
+      let segmentCompleteInVVT = await osap.nr.find("ep_segmentCompleteIn", graph)
+      for (let actu of actuators) {
+        let segmentCompletOutVVT = await osap.nr.findWithin("ep_segmentCompleteOut", actu.settings.name, graph)
+        let connectRoute = await osap.nr.findRoute(segmentCompletOutVVT, segmentCompleteInVVT)
+        // lower priority than halt signals, but higher than general purpose 
+        connectRoute.ttl = 1000
+        connectRoute.mode = EP.ROUTEMODE_ACKLESS
+        await osap.mvc.setEndpointRoute(segmentCompletOutVVT.route, connectRoute)
+        console.log(`SETUP: connected ${actu.settings.name} segmentComplete to JS`)
+      }
+      console.log(`SETUP: queue complete are piped...`)
+      // ---------------------------------------- END 
+      this.available = true
+    } catch (err) {
+      throw err
+    }
+    /* 
+    (1) setup each actuator, right? 
+    (2) setup check SPU & over-ticking... or is that motor responsibility ? 
+    (3) plumb our moveOutEP to broadcast to actuator inputs, 
+    (4) plumb state update request EP likewise... 
+    (5) plumb our stateOutEP likewise... 
+    */
+  } // ------------------------------------------ End of Setup 
+
+  this.home = async (graph) => {
+    try {
+      // ---------------------------------------- Let's home it just here... 
+      console.log(`HOME: homing ! `)
+      // we're going to need another... 
+      console.log(`HOME: collecting a graph...`)
+      if (!graph) graph = await osap.nr.sweep()
+      // for this machine... which TODO home routines should go in some super-object,
+      // i.e. ClankFXYVM.js which should use AXLCore.js as sub, but w/ 
+      // let's hook each limit to itself, har, we can use the same route per actu, 
+      // but want to search for indices to be fancy 
+      let firstActuName = actuators[0].settings.name
+      let haltInIndice = (await osap.nr.findWithin("ep_haltIn", firstActuName, graph)).indice
+      // console.log(limitOutputIndice, haltInIndice)
+      // so we can dead-nuts this route:
+      let ownLimitRoute = PK.route().sib(haltInIndice).end()
+      // and we can add 'em to each actuator, 
+      for (let actu of actuators) {
+        let limitOutVVT = await osap.nr.findWithin("ep_limitSwitchState", actu.settings.name, graph)
+        await osap.mvc.setEndpointRoute(limitOutVVT.route, ownLimitRoute)
+      }
+      // z... 
+      let zHomeRate = 1.5
+      console.warn("HOMING Z....")
+      await Promise.all([
+        actuators[3].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[4].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[5].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[6].gotoVelocity([0, 0, zHomeRate, 0]),
+      ])
+      await Promise.all([
+        actuators[3].awaitMotionEnd(),
+        actuators[4].awaitMotionEnd(),
+        actuators[5].awaitMotionEnd(),
+        actuators[6].awaitMotionEnd(),
+      ])
+      // double tap, 
+      await Promise.all([
+        actuators[3].gotoVelocity([0, 0, -5, 0]),
+        actuators[4].gotoVelocity([0, 0, -5, 0]),
+        actuators[5].gotoVelocity([0, 0, -5, 0]),
+        actuators[6].gotoVelocity([0, 0, -5, 0]),
+      ])
+      await TIME.delay(500)
+      await Promise.all([
+        actuators[3].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[4].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[5].gotoVelocity([0, 0, zHomeRate, 0]),
+        actuators[6].gotoVelocity([0, 0, zHomeRate, 0]),
+      ])
+      await Promise.all([
+        actuators[3].awaitMotionEnd(),
+        actuators[4].awaitMotionEnd(),
+        actuators[5].awaitMotionEnd(),
+        actuators[6].awaitMotionEnd(),
+      ])
+      // then we can run... x 
+      console.warn("HOMING X...")
+      await actuators[0].gotoVelocity([20, 0, 0, 0])
+      await actuators[0].awaitMotionEnd()
+      // double tap, 
+      await actuators[0].gotoVelocity([-20, 0, 0, 0])
+      await TIME.delay(500)
+      await actuators[0].gotoVelocity([20, 0, 0, 0])
+      await actuators[0].awaitMotionEnd()
+      console.warn("X Halted...")
+      console.warn("HOMING Y...")
+      await Promise.all([actuators[1].gotoVelocity([0, 10, 0, 0]), actuators[2].gotoVelocity([0, 10, 0, 0])])
+      await Promise.all([actuators[1].awaitMotionEnd(), actuators[2].awaitMotionEnd()])
+      // double tap, 
+      await Promise.all([actuators[1].gotoVelocity([0, -10, 0, 0]), actuators[2].gotoVelocity([0, -10, 0, 0])])
+      await TIME.delay(500)
+      await Promise.all([actuators[1].gotoVelocity([0, 10, 0, 0]), actuators[2].gotoVelocity([0, 10, 0, 0])])
+      await Promise.all([actuators[1].awaitMotionEnd(), actuators[2].awaitMotionEnd()])
+      console.warn("Y Halted...")
+      // we have to mess w/ the extruder, else it will stay in MODE_POS w/ TARG = 0... 
+      await actuators[7].gotoVelocity([0, 0, 0, 0])
+      // now we can set position... 
+      await this.setPosition(this.settings.bounds, true)
+      parkPosition[0] = this.settings.bounds[0] - 80
+      parkPosition[1] = this.settings.bounds[1] - 80
+      parkPosition[2] = this.settings.bounds[2] - 10
+      await this.park()
+      console.warn(`------------------------------------------`)
+      console.warn(`DONE`)
+      return
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Inputs and Outputs 
+  // ingest moves, 
+  // let moveInEP = osap.endpoint("unplannedMoves") 
+  // moveInEP.onData = async (data) => {
+  //   try {
+  //     // TODO here: invent unplanned-move serialization & ingestion... 
+  //     // or... leave this off as a js-input that can become something else ? 
+  //     await queue.notFull() // lol, idk, 
+  //   } catch (err) {
+  //     console.error(err)
+  //   }
+  // }
+  // js API... also: our endpoint can just call this, 
+  // unplannedMove = { target: [numDof], rate: <num> }
+
+  // ingest halt signals, 
+  /*
+  #define AXL_HALT_NONE 0 
+  #define AXL_HALT_SOFT 1 
+  #define AXL_HALT_CASCADE 3 
+  #define AXL_HALT_ACK_NOT_PICKED 4
+  #define AXL_HALT_MOVE_COMPLETE_NOT_PICKED 5
+  #define AXL_HALT_BUFFER_STARVED 6
+  #define AXL_HALT_OUT_OF_ORDER_ARRIVAL 7 
+  */
+  let haltInEP = osap.endpoint("haltInJS")
+  let haltOutEP = osap.endpoint("haltOutJS")
+  let haltCodes = [
+    "none",
+    "soft",
+    "cascade",
+    "ack not picked",
+    "move complete not picked",
+    "buffer starved",
+    "out of order arrival"
+  ]
+
+  haltInEP.onData = (data) => {
+    // TODO here: ingest string halting-reason-message, 
+    // then mirror-out, 
+    let message = haltCodes[data[0]]
+    let str = TS.read("string", data, 1).value
+    console.error(`HALT! ${message}, with message: ${str}`)
+    queueState = QUEUE_STATE_HALTED
+    //haltOutEP.write("_") // if !halted already, send a pozi edge, if halted, donot repeat msg 
+  }
+
+  // handler to replace, 
+  this.onNetInfoUpdate = (info) => { }
+
+  let netInfo = {
+    rtt: 0,
+    rttMin: Infinity,
+    rttMax: 0,
+  }
+
+  // planned-move outputs, 
+  let segmentsOutEP = osap.endpoint("segmentsOut")
+  // segment complete back 
+  let segmentCompleteInEP = osap.endpoint("segmentCompleteIn")
+  segmentCompleteInEP.onData = (data) => {
+    // console.warn(`received queue complete at ${TIME.getTimeStamp()}`, data)
+    let msgSegmentNumber = TS.read('uint32', data, 0)
+    let msgActuatorID = TS.read('uint8', data, 4)
+    // console.log(`segNum; ${msgSegmentNumber}, actuID; ${msgActuatorID}`)
+    // find eeeet, and it should always be the most recent, right?
+    console.warn(`segmentComplete from ${msgActuatorID}, segNum ${msgSegmentNumber}`)
+    if (queue[0].segmentNumber != msgSegmentNumber) {
+      throw new Error(`! retrieved out-of-order segmentComplete msg, probable failure?`)
+    } else {
+      // get stats... 
+      let outTime = TIME.getTimeStamp() - queue[0].transmitTime
+      // console.warn(`segmentComplete ${msgSegmentNumber}, outTime was ${outTime}ms`)
+      queue.shift()
+      checkQueueState()
+    }
+  }
+  // segment ack back
+  let segmentAckInEP = osap.endpoint("segmentAckIn")
+  segmentAckInEP.onData = (data) => {
+    // console.warn(`received queue ack at ${TIME.getTimeStamp()}`, data)
+    let msgSegmentNumber = TS.read('uint32', data, 0)
+    let msgActuatorID = TS.read('uint8', data, 4)
+    // we could ask for more data here... like current state ?
+    // or / we should combine current state w/ these... i.e. some policy like:
+    // at-least once / 10ms we (1) get state from a drop or (2) rx one of these messages, which includes state... 
+    // which is this ?
+    for (let m in queue) {
+      if (queue[m].segmentNumber == msgSegmentNumber) {
+        let rtt = TIME.getTimeStamp() - queue[m].transmitTime
+        netInfo.rtt = netInfo.rtt * 0.975 + rtt * 0.025
+        if (rtt < netInfo.rttMin) netInfo.rttMin = rtt
+        if (rtt > netInfo.rttMax) netInfo.rttMax = rtt
+        this.onNetInfoUpdate(JSON.parse(JSON.stringify(netInfo)))
+        // console.warn(`segmentAck ${msgSegmentNumber}, rtt was ${rtt}ms`)
+        return
+      }
+    }
+    throw new Error(`apparently no match for ${msgSegmentNumber}`)
+  }
+
+  // would do one of these each for actuators, right? 
+  let actuatorStateEP = osap.endpoint("stateInput")
+  actuatorStateEP.onData = (data) => {
+    console.warn(`received actuator data`, data)
+  }
+
+  // our state output...
+  let stateRequestsOutEP = osap.endpoint("stateRequestsOut")
+
+  // ------------------------------------------------------ Modal / State 
+
+  let AXL_MODE_ACCEL = 1
+  let AXL_MODE_VELOCITY = 2
+  let AXL_MODE_POSITION = 3
+  let AXL_MODE_QUEUE = 4
+
+  this.writeStateBroadcast = async (vals, mode, set) => {
+    try {
+      // pack 'em up, 
+      let datagram = new Uint8Array(numDof * 4 + 2)
+      let wptr = 0
+      datagram[wptr++] = mode
+      datagram[wptr++] = set
+      for (let a = 0; a < numDof; a++) {
+        wptr += TS.write("float32", vals[a], datagram, wptr)
+      }
+      // and send it along on our broadcast channel, 
+      await stateRequestsOutEP.write(datagram, "ackless")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.halt = async () => {
+    try {
+      await haltOutEP.write(new Uint8Array([1]))
+      await this.awaitMotionEnd()
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.gotoVelocity = async (vel) => {
+    try {
+      vel = this.cartesianToActuatorTransform(vel)
+      await this.writeStateBroadcast(vel, AXL_MODE_VELOCITY, false)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.gotoPosition = async (pos) => {
+    try {
+      // transform posn vals, 
+      let actuPos = this.cartesianToActuatorTransform(pos, true)
+      console.warn(`pos -> actuators: ${pos[0].toFixed(2)} ${pos[1].toFixed(2)}  -> ${actuPos[0].toFixed(2)} ${actuPos[1].toFixed(2)}`)
+      // console.warn(`gotoPosition`, JSON.parse(JSON.stringify(pos)))
+      await this.writeStateBroadcast(actuPos, AXL_MODE_POSITION, false)
+      await TIME.delay(10)
+      await this.awaitMotionEnd()
+      mostRecentPosition = JSON.parse(JSON.stringify(actuPos))
+      // TODO: for some reason this doesn't get all the way to the target, 
+      // the first time we call it ? something something transforms, maybe ? 
+      // let res = await this.getPosition()
+      // console.log(`wentTo`, res)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.park = async () => {
+    try {
+      let pos = await this.getPosition()
+      // z first... 
+      await this.gotoPosition([pos[0], pos[1], parkPosition[2], pos[3]])
+      parkPosition[3] = pos[3]
+      await this.gotoPosition(parkPosition)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.setPosition = async (pos, override) => {
+    try {
+      await this.awaitMotionEnd()
+      if (override) {
+        await this.writeStateBroadcast(pos, AXL_MODE_POSITION, true)
+      } else {
+        // we... might not have to do anything here ? or is just todo with the stored offsets... 
+        throw new Error(`y'all haven't written this yet ?`)
+      }
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.awaitMotionEnd = async () => {
+    // each actuator should have one... 
+    let promises = []
+    for (let actu of actuators) {
+      promises.push(actu.awaitMotionEnd())
+    }
+    await Promise.all(promises)
+  }
+
+  this.getPosition = async () => {
+    try {
+      // let actuState = await actuators[0].getStates()
+      let actuatorStates = await Promise.all([actuators[0].getStates(), actuators[1].getStates(), actuators[3].getStates(), actuators[7].getStates()])
+      let posEst = new Array(numDof)
+      for (let a = 0; a < numDof; a++) {
+        let sum = 0
+        for (let state of actuatorStates) {
+          sum += state.positions[a]
+        }
+        posEst[a] = sum / numDof
+      }
+      console.log(`POS EST`, posEst)
+      // pls tell jake if this fires, 
+      for (let a = 0; a < numDof; a++) {
+        for (let state of actuatorStates) {
+          if (Math.abs(state.positions[a] - posEst[a]) > 0.1) {
+            throw new Error(`heyo, states are drifting, please inspect ${state.positions[a]} ${posEst[a]}`)
+          }
+        }
+      }
+      // do inverse... 
+      return this.actuatorToCartesianTransform(posEst, true)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // I think we just set our own internal offsets ? 
+  this.setZPosition = async (zPos) => {
+    try {
+      let pos = await this.getPosition()
+      pos = JSON.parse(JSON.stringify(pos))
+      console.log(`pos est, `, pos)
+      // uuuh, we take the difference between ... 
+      console.warn(`prev z-offset \t${positionOffset[2]}`)
+      positionOffset[2] += zPos - pos[2]
+      parkPosition[2] = positionOffset[2]
+      console.warn(`new z-offset \t${positionOffset[2]}`)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // deltas: [<x>, <y>, <z>]
+  this.moveRelative = async (deltas) => {
+    try {
+      await this.awaitMotionEnd()
+      let current = await this.getPosition()
+      for (let axis in deltas) {
+        current[axis] += deltas[axis]
+      }
+      await this.gotoPosition(current)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.disableMotors = async () => {
+    try {
+      for (let actu of actuators) {
+        actu.settings.cscale = 0 
+        await actu.setupMotor()
+      }
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // ------------------------------------------------------ Queue Updates 
+
+  let QUEUE_STATE_EMPTY = 1
+  let QUEUE_STATE_AWAITING_START = 2
+  let QUEUE_STATE_RUNNING = 3
+  let QUEUE_STATE_HALTED = 4
+
+  let AXL_REMOTE_QUEUE_MAX_LENGTH = 32
+
+  // we have a queue of moves... 
+  let queue = []
+  let mostRecentPosition = []
+  let maxQueueLength = 128
+  let jsQueueStartDelay = 1000
+  let queueState = QUEUE_STATE_EMPTY
+  let nextSegmentNumber = 0
+  let nextReturnActuator = 0
+
+  this.addMoveToQueue = async (unplannedMove) => {
+    return new Promise((resolve, reject) => {
+      let check = async () => {
+        if (queue.length < maxQueueLength) {
+          // transform the target pos... 
+          let actuPos = this.cartesianToActuatorTransform(unplannedMove.target, true)
+          console.warn(`move -> actuators: ${unplannedMove.target[0].toFixed(2)} ${unplannedMove.target[1].toFixed(2)}  -> ${actuPos[0].toFixed(2)} ${actuPos[1].toFixed(2)}`)
+          // the hack, 
+          let hackCornerVel = unplannedMove.rate * 0.25 > 15 ? unplannedMove.rate * 0.25 : 15;
+          let hackMaxVel = unplannedMove.rate > 15 ? unplannedMove.rate : 15;
+          if (unplannedMove.rate < 15) {
+            hackCornerVel = unplannedMove.rate
+            hackMaxVel = unplannedMove.rate
+          }
+          // this would mean that I whiffed it;
+          if (hackCornerVel > hackMaxVel) throw new Error(`cmon man, ${hackCornerVel}, ${hackMaxVel}`)
+          // console.log(`vels: ${hackCornerVel}, ${hackMaxVel}`)
+          // ingest it here... 
+          let segment = {
+            endPos: actuPos,                                  // where togo (upfront transform)
+            vi: hackCornerVel,                                // start... 
+            accel: 750,                                       // accel-rate 
+            vmax: hackMaxVel,                                 // max-rate
+            vf: hackCornerVel,                                // end-rate 
+            segmentNumber: nextSegmentNumber,                 // # in infinite queue
+            isLastSegment: false,                             // is it the end of queue ? remotes use to figure if starvation is starvation
+            returnActuator: nextReturnActuator,               // which actuator should ack us... this should be rolling as well, 
+            transmitTime: 0,                                  // when did it depart... (for JS, not serialized)
+          }
+          // increment this... 
+          nextSegmentNumber++
+          // and that 
+          nextReturnActuator++; if (nextReturnActuator >= actuators.length) nextReturnActuator = 0;
+          // we need a distance and unit vector, so we need to know previous, 
+          let previous = {}
+          if (queue[queue.length - 1]) {
+            previous = queue[queue.length - 1]
+          } else {
+            previous = {
+              endPos: JSON.parse(JSON.stringify(mostRecentPosition)),  // wherever we were last, 
+              vf: 0.0
+            }
+          }
+          mostRecentPosition = JSON.parse(JSON.stringify(segment.endPos))
+          segment.distance = distance(previous.endPos, segment.endPos)
+          segment.unitVector = unitVector(previous.endPos, segment.endPos)
+          // console.log(`from `, previous.endPos, `to `, segment.endPos, `dist ${dist.toFixed(2)}`, unit)
+          // can calculate distance, deltas, and unit vector... 
+          queue.push(segment)
+          // do a "re-plan" which for now is very simple... 
+          runQueueOptimization()
+          // console.warn(`AXL Core ingests ${queue.length} / ${maxQueueLength}`)
+          // this is async because it transmits out the other end... 
+          checkQueueState().then(() => {
+            resolve()
+          }).catch((err) => {
+            reject(err)
+          })
+        } else {
+          setTimeout(check, 0)
+        }
+      }
+      check()
+    })
+  }
+
+  let checkQueueState = async () => {
+    try {
+      switch (queueState) {
+        case QUEUE_STATE_EMPTY:
+          if (queue.length > 0) {
+            queueState = QUEUE_STATE_AWAITING_START
+            setTimeout(() => {
+              console.warn(`QUEUE START FROM AWAITING...`)
+              queueState = QUEUE_STATE_RUNNING
+              checkQueueState()
+            }, jsQueueStartDelay)
+          }
+          break;
+        case QUEUE_STATE_AWAITING_START:
+          // noop, wait for timer... 
+          break;
+        case QUEUE_STATE_RUNNING:
+          // can we publish, do we have unplanned, etc?
+          // console.warn(`QUEUE RUNNING...`)
+          // so we'll try to transmit up to 32 ? and just stuff 'em unapologetically into the buffer, leggo: 
+          for (let m = 0; m < AXL_REMOTE_QUEUE_MAX_LENGTH - 1; m++) {
+            if (!queue[m]) {
+              // console.warn(`breaking because not-even-32-items here...`)
+              break;
+            }
+            if (queue[m].transmitTime == 0) {
+              // console.log(`tx'd item at ${m}, segment ${queue[m].segmentNumber}`)
+              await transmitSegment(queue[m])
+            }
+          }
+          break;
+        case QUEUE_STATE_HALTED:
+          console.warn(`halted, exiting...`)
+          break;
+        default:
+          console.error(`unknown state...`)
+          break;
+      } // end switch 
+    } catch (err) {
+      throw err
+    }
+  }
+
+  let transmitSegment = async (seg) => {
+    try {
+      // then... serialize and transmit it, right?
+      let datagram = new Uint8Array(4 + 1 + 1 + numDof * 4 + 5 * 4)
+      let wptr = 0
+      // segnum, return actuator, unit vect, vi, accel, vmax, vf, distance, done 
+      wptr += TS.write("uint32", seg.segmentNumber, datagram, wptr)
+      wptr += TS.write("uint8", seg.returnActuator, datagram, wptr)
+      wptr += TS.write("boolean", seg.isLastSegment, datagram, wptr)
+      for (let a = 0; a < numDof; a++) {
+        wptr += TS.write("float32", seg.unitVector[a], datagram, wptr)
+      }
+      wptr += TS.write("float32", seg.vi, datagram, wptr)
+      wptr += TS.write("float32", seg.accel, datagram, wptr)
+      wptr += TS.write("float32", seg.vmax, datagram, wptr)
+      wptr += TS.write("float32", seg.vf, datagram, wptr)
+      wptr += TS.write("float32", seg.distance, datagram, wptr)
+      // write that, ackless, to the pmo
+      await segmentsOutEP.write(datagram)
+      seg.transmitTime = TIME.getTimeStamp()
+      console.warn(`TX'd ${seg.segmentNumber} at ${seg.transmitTime} with last ? ${seg.isLastSegment} vf ${seg.vf}, vi ${seg.vi}, return from ${seg.returnActuator}`)
+      // HERE is an OSAP TODO, which causes us to loose ~ ms of performance: because 
+      // time stamps in packets are ms-based, we can't send multiple packets in the same `ms` 
+      // while also retaining FIFO-ness. We should rather have ns, us, and ms in the transport layer timestamps... 
+      // so do some analysis on data lengths etc (max packet life? min gap?) and also see how to get 
+      // ns times in JS, etc... 
+      await TIME.delay(0)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // this is dirt simple for now, we simply ensure that end velocities are 0... 
+  let runQueueOptimization = () => {
+    for (let m = 0; m < queue.length; m++) {
+      // for anything which hasn't yet transmitted, 
+      if (queue[m].transmitTime == 0) {
+        if (m == queue.length - 1) {
+          // queue[m].vf = 0.0
+          queue[m].isLastSegment = true
+        } else {
+          // queue[m].vf = 50.0 // do "fixed-bang-at-corner" junction deviation ?
+          queue[m].isLastSegment = false
+        }
+      }
+    }
+  }
+
+}
+
+// ------------------------------------ Planning... Utes?
+
+/*
+// given accel, final rate, and distance, how big is vi?
+float maxVi(float accel, float vf, float distance){
+  //OSAP::debug(String(accel) + " " + String(vf) + " " + String(distance));
+  return sqrtf(vf * vf - 2.0F * accel * distance);
+}
+*/
+
+// between A and B 
+let distance = (A, B) => {
+  let sum = 0
+  for (let a = 0; a < numDof; a++) {
+    sum += Math.pow((A[a] - B[a]), 2)
+  }
+  return Math.sqrt(sum)
+}
+
+// from A to B 
+let unitVector = (A, B) => {
+  let dist = distance(A, B)
+  let unit = new Array(numDof)
+  for (let a = 0; a < numDof; a++) {
+    unit[a] = (B[a] - A[a]) / dist
+  }
+  return unit
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/axlMotionHeadVM.js b/system/javascript/osapjs/vms/axlMotionHeadVM.js
new file mode 100644
index 0000000000000000000000000000000000000000..bcdeb3b71888a241b46186b167b41ccb3247d2cf
--- /dev/null
+++ b/system/javascript/osapjs/vms/axlMotionHeadVM.js
@@ -0,0 +1,60 @@
+/*
+axlMotionHeadVM
+
+holonic motion control coordinator virtual machine
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../core/ts.js'
+import PK from '../core/packets.js'
+import TIME from '../core/time.js'
+import AXLMotionVM from './axlMotionVM.js'
+
+export default function AXLMotionHeadVM(osap, route, _settings, useMiddleFifo = false) {
+  // this FW has a motion coordinator on board, 
+  this.motion = new AXLMotionVM(osap, route, _settings, useMiddleFifo)
+
+  // and some bonus power-switching capability... 
+  let powerEP = osap.endpoint("powerMirror") 
+  powerEP.addRoute(PK.route(route).sib(6).end())
+  let powerQuery = osap.query(PK.route(route).sib(6).end())
+
+  this.setPowerStates = (v5, v24) => {
+    // 5v on / off, 24v on / off, 
+    let wptr = 0;
+    let datagram = new Uint8Array(2)
+    wptr += TS.write('boolean', v5, datagram, wptr, true)
+    wptr += TS.write('boolean', v24, datagram, wptr, true)
+    return new Promise((resolve, reject) => {
+      powerEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.getPowerStates = () => {
+    return new Promise((resolve, reject) => {
+      powerQuery.pull().then((data) => {
+        resolve(data) 
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // ------------------------------------------------------ SETUP 
+  this.setup = async () => {
+    try {
+      //await this.motion.setup()
+      await this.setPowerStates(true, false)
+    } catch (err) {
+      console.error(err)
+      throw err 
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/axlMotionVM.js b/system/javascript/osapjs/vms/axlMotionVM.js
new file mode 100644
index 0000000000000000000000000000000000000000..b729ba2d4d37865967ac7b50f02775489c9d4d76
--- /dev/null
+++ b/system/javascript/osapjs/vms/axlMotionVM.js
@@ -0,0 +1,250 @@
+/*
+axlMotionVM
+
+holonic motion control coordinator virtual machine
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS, EP } from '../core/ts.js'
+import PK from '../core/packets.js'
+import { settingsDiff } from '../utes/diff.js'
+
+let AXL_MODE_ACCEL = 1
+let AXL_MODE_VELOCITY = 2
+let AXL_MODE_POSITION = 3
+let AXL_MODE_QUEUE = 4
+
+export default function AXLMotionVM(osap, route, _settings, useMiddleFifo = false) {
+
+  // defaults, 
+  this.settings = {
+    junctionDeviation: 0.05,
+    accelLimits: [100, 100, 100, 100],
+    velLimits: [100, 100, 100, 100]
+  }
+
+  // if present, check against 
+  if (_settings) {
+    settingsDiff(this.settings, _settings, "axlMotionVM")
+    this.settings = JSON.parse(JSON.stringify(_settings))
+  }
+  
+  let numDof = this.settings.accelLimits.length 
+
+  // -------------------------------------------- States
+
+  let setStatesEP = osap.endpoint("axlStateMirror")
+  setStatesEP.addRoute(PK.route(route).sib(2).end())
+
+  this.writeStates = (mode, vals, set = false) => {
+    return new Promise((resolve, reject) => {
+      if (vals.length != numDof) {
+        reject(`need array of len ${numDof} dofs, was given ${vals.length}`);
+        return;
+      }
+      // pack, 
+      let datagram = new Uint8Array(numDof * 4 + 2)
+      datagram[0] = mode
+      // set, or target?
+      set ? datagram[1] = 1 : datagram[1] = 0;
+      // write args... 
+      for (let a = 0; a < numDof; a++) {
+        TS.write("float32", vals[a], datagram, a * 4 + 2)
+      }
+      // ship it, 
+      setStatesEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.broadcastStates = (mode, vals, route, set = false) => {
+    console.warn(`! this needs an improvement / more thought...`)
+    if (vals.length != numDof) {
+      reject(`need array of len ${numDof} dofs, was given ${vals.length}`);
+      return;
+    }
+    // pack, 
+    let payload = new Uint8Array(numDof * 4 + 2 + 2)
+    let wptr = 0
+    payload[0] = PK.DEST
+    payload[1] = EP.SS_ACKLESS
+    payload[2] = mode
+    // set, or target?
+    payload[3] = set ? 1 : 0;
+    // write args... 
+    for (let a = 0; a < numDof; a++) {
+      TS.write("float32", vals[a], payload, a * 4 + 2 + 2)
+    }
+    // make packet, 
+    let datagram = PK.writeDatagram(route, payload)
+    PK.logPacket(datagram)
+    // handle it?
+    osap.handle(datagram)
+  }
+
+  this.broadcastVelocity = (vels, route) => {
+    return this.broadcastStates(AXL_MODE_VELOCITY, vels, route)
+  }
+
+  this.setPosition = async (posns) => {
+    await this.awaitMotionEnd()
+    return this.writeStates(AXL_MODE_POSITION, posns, true)
+  }
+
+  this.targetPosition = (posns) => {
+    return this.writeStates(AXL_MODE_POSITION, posns, false)
+  }
+
+  this.targetVelocity = (vels) => {
+    return this.writeStates(AXL_MODE_VELOCITY, vels, false)
+  }
+
+  let statesQuery = osap.query(PK.route(route).sib(2).end())
+  this.getStates = () => {
+    return new Promise((resolve, reject) => {
+      statesQuery.pull().then((data) => {
+        let states = {
+          positions: [],
+          velocities: [],
+          accelerations: []
+        }
+        switch (data[0]) {
+          case AXL_MODE_POSITION:
+            states.mode = "position"
+            break;
+          case AXL_MODE_ACCEL:
+            states.mode = "accel"
+            break;
+          case AXL_MODE_VELOCITY:
+            states.mode = "velocity"
+            break;
+          case AXL_MODE_QUEUE:
+            states.mode = "queue"
+            break;
+          default:
+            states.mode = "unrecognized"
+            break;
+        }
+        data[1] ? states.motion = true : states.motion = false;
+        for (let a = 0; a < numDof; a++) {
+          states.positions.push(TS.read("float32", data, a * 4 + numDof * 4 * 0 + 2))
+        }
+        for (let a = 0; a < numDof; a++) {
+          states.velocities.push(TS.read("float32", data, a * 4 + numDof * 4 * 1 + 2))
+        }
+        for (let a = 0; a < numDof; a++) {
+          states.accelerations.push(TS.read("float32", data, a * 4 + numDof * 4 * 2 + 2))
+        }
+        resolve(states)
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.awaitMotionEnd = () => {
+    return new Promise((resolve, reject) => {
+      let check = () => {
+        this.getStates().then((states) => {
+          if (states.motion) {
+            setTimeout(check, 5)
+          } else {
+            resolve()
+          }
+        })
+      }
+      check()
+    })
+  }
+
+  // -------------------------------------------- Halt 
+
+  let haltEP = osap.endpoint("axlHaltMirror")
+  haltEP.addRoute(PK.route(route).sib(3).end())
+  this.halt = async () => {
+    try {
+      await haltEP.write(new Uint8Array([1]), "acked");
+      console.warn(`wrote halt... awaiting motion end`)
+      await this.awaitMotionEnd()
+      console.warn(`motion-ended`)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // -------------------------------------------- Add Move
+
+  let addMoveEP = osap.endpoint("axlMoveMirror")
+  console.log(addMoveEP)
+  if(!useMiddleFifo) addMoveEP.addRoute(PK.route(route).sib(4).end())
+  addMoveEP.setTimeoutLength(60000)
+  // hackney, 
+  let lastPos = [0, 0, 0, 0]
+  let liftZ = 2
+  let lastTheta = 0
+  let thetaIncrements = 0
+  // move like { target: <float array of len numDof>, rate: <number> }
+  this.addMoveToQueue = async (move) => {
+    try {
+      // I'm going to hack this up to add theta-down-move-up junctions everywhere... 
+      if (move.target.length != numDof) {
+        throw new Error(`move has ${move.target.length} dofs, AXL is config'd for ${numDof}`);
+      }
+      let datagram = new Uint8Array(numDof * 4 + 4)
+      TS.write("float32", move.rate, datagram, 0)
+      for (let a = 0; a < numDof; a++) {
+        TS.write("float32", move.target[a], datagram, 4 * a + 4)
+      }
+      await addMoveEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // -------------------------------------------- Settings
+
+  let settingsEP = osap.endpoint("axlSettingsMirror")
+  settingsEP.addRoute(PK.route(route).sib(5).end())
+  this.setup = async () => {
+    // console.log(this.settings.accelLimits)
+    let datagram = new Uint8Array(4 + numDof * 4 * 2)
+    TS.write("float32", this.settings.junctionDeviation, datagram, 0);
+    for (let a = 0; a < numDof; a++) {
+      TS.write("float32", this.settings.accelLimits[a], datagram, 4 + a * 8)
+      TS.write("float32", this.settings.velLimits[a], datagram, 4 + a * 8 + 4)
+    }
+    //console.warn('gram', datagram)
+    try {
+      // if we need to find the fifo,
+      if(useMiddleFifo){
+        // awkwardly, we have to trigger a re-sweep here because 
+        console.warn(`with this setup-speed problem, the sweep-after-setup is a good start`)
+        await osap.nr.forceSweepUpdate()
+        console.log(`finding the fifo...`)
+        let moveVVT = await osap.nr.find("ep_axlMoveMirror")
+        let inVVT = await osap.nr.find("ep_fifoInput")
+        let upperLink = await osap.nr.findRoute(moveVVT, inVVT)
+        PK.logRoute(upperLink)
+        addMoveEP.addRoute(upperLink)
+        console.log(`found route from ourself and the fifo input...`)
+        // now look for the other end, 
+        let outVVT = await osap.nr.find("ep_fifoOutput")
+        let headVVT = await osap.nr.findWithin("ep_moves", "rt_motion-head")
+        let lowerLink = await osap.nr.findRoute(outVVT, headVVT)
+        PK.logRoute(lowerLink)
+        await osap.mvc.setEndpointRoute(outVVT.route, lowerLink)
+        console.log(`plumbed from fifo-out to head-in, I think`)
+      }
+      await settingsEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/axlMotorVM.js b/system/javascript/osapjs/vms/axlMotorVM.js
new file mode 100644
index 0000000000000000000000000000000000000000..3df22d97cba62ea00177c1803074123224f57d68
--- /dev/null
+++ b/system/javascript/osapjs/vms/axlMotorVM.js
@@ -0,0 +1,88 @@
+/*
+axlMotorVM
+
+holonic motion control motor virtual machine
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../core/ts.js'
+import PK from '../core/packets.js'
+import TIME from '../core/time.js'
+import AXLMotionVM from './axlMotionVM.js'
+import { settingsDiff } from '../utes/diff.js'
+
+export default function AXLMotorVM(osap, route, _settings) {
+  // same settings as the coordinator... 
+  this.motion = new AXLMotionVM(osap, route, _settings.motion)
+  // defaults,
+  this.settings = {
+    motion: {
+      junctionDeviation: 0.05, 
+      accelLimits: [2500, 2500, 2500],
+      velLimits: [100, 100, 100]
+    },
+    axis: 0,
+    invert: false,
+    microstep: 4,
+    spu: 20,
+    cscale: 0.25,
+    homeRate: -100,
+    homeOffset: 100, 
+  }
+
+  // we want to diff our settings... 
+  if(_settings) {
+    // this throws an error if we miss anything 
+    settingsDiff(this.settings, _settings, "axlMotorVM")
+    this.settings = JSON.parse(JSON.stringify(_settings))
+  }
+
+  let settingsEP = osap.endpoint()
+  settingsEP.addRoute(PK.route(route).sib(6).end())
+
+  this.setup = async () => {
+    try {
+      // setup the local integrator settings... 
+      await this.motion.setup()
+      // do also this, 
+      let datagram = new Uint8Array(12)
+      TS.write('uint8', this.settings.axis, datagram, 0)
+      TS.write('boolean', this.settings.invert, datagram, 1)
+      TS.write('uint16', this.settings.microstep, datagram, 2)
+      TS.write('float32', this.settings.spu, datagram, 4)
+      TS.write('float32', this.settings.cscale, datagram, 8)
+      await settingsEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+
+  let homeEP = osap.endpoint()
+  homeEP.addRoute(PK.route(route).sib(7).end())
+
+  this.home = async () => {
+    try {
+      await this.motion.awaitMotionEnd()
+      //console.warn(`motor home: awaited motion end`)
+      let datagram = new Uint8Array(9)
+      datagram[0] = this.settings.axis
+      //console.warn(`setting rate, offset ${this.settings.homeRate}, ${this.settings.homeOffset}`)
+      TS.write('float32', this.settings.homeRate, datagram, 1)
+      TS.write('float32', this.settings.homeOffset, datagram, 5)
+      await homeEP.write(datagram, "acked")
+      //console.warn(`wrote to homeEP`)
+      await TIME.delay(250)
+      await this.motion.awaitMotionEnd()
+      //console.warn(`motion ended`)
+    } catch (err) {
+      throw err 
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/bladePlateVirtualMachine.js b/system/javascript/osapjs/vms/bladePlateVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f93bdaf9e5d2129cc4f1c157698b580f9892a56
--- /dev/null
+++ b/system/javascript/osapjs/vms/bladePlateVirtualMachine.js
@@ -0,0 +1,55 @@
+/*
+bladePlateVirtualMachine.js
+
+vm for hotplate toolchanger circuit 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import TIME from '../../osapjs/core/time.js'
+import PK from '../../osapjs/core/packets.js'
+
+export default function BladePlateVM(osap, route){
+  // set current for servo actuator 
+  let servoMicrosecondsEP = osap.endpoint()
+  servoMicrosecondsEP.addRoute(PK.route(route).sib(2).end())
+  this.writeServoMicroseconds = (micros) => {
+    return new Promise((resolve, reject) => {
+      let datagram = new Uint8Array(4)
+      TS.write('uint32', micros, datagram, 0, true)
+      servoMicrosecondsEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.config = {} 
+  this.config.microsClosed = 2000;
+  this.config.microsOpen = 1000;
+
+  this.setMicroSet = (closed, open) => {
+    this.config.microsClosed = closed 
+    this.config.microsOpen = open 
+  }
+
+  this.setLeverState = async (closed) => {
+    try {
+      if(closed) { 
+        await this.writeServoMicroseconds(this.config.microsClosed)
+      } else {
+        await this.writeServoMicroseconds(this.config.microsOpen)
+      }
+      // takes ~ some time to close / open 
+      await TIME.delay(1000)
+    } catch (err) {
+      throw err 
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/escDropVM.js b/system/javascript/osapjs/vms/escDropVM.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e2c09e98d140d08d93f2a59bc988916adfe9bcb
--- /dev/null
+++ b/system/javascript/osapjs/vms/escDropVM.js
@@ -0,0 +1,40 @@
+/*
+escDropVM.js
+
+vm for barebones esc-controller module
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import PK from '../../osapjs/core/packets.js'
+
+export default function ESCDropVM(osap, route) {
+  let dutyEP = osap.endpoint("dutyMirror")
+  dutyEP.addRoute(PK.route(route).sib(2).end())
+  // PK.logRoute(route)
+
+  this.setDuty = async (duty) => {
+    try {
+      let datagram = new Uint8Array(4)
+      TS.write('float32', duty, datagram, 0)
+      await dutyEP.write(datagram, "acked")
+    } catch (err) {
+      throw err 
+    }
+  }
+}
+
+// 0.1 -> nok RPM
+// 0.15 -> 2.5k RPM ~ unstable stops 
+// 0.2 -> 6k RPM 
+// 0.3 -> 12k RPM 6k comfortable, low harmonic 
+// 0.4 -> 18k RPM 6k p loud 
+// 0.5 -> 23k RPM 5k loud AF 
+// 0.6 -> don't do this
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/filamentSensorVirtualMachine.js b/system/javascript/osapjs/vms/filamentSensorVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..819070759bbefa18f5a43d2e39f3bab06063d184
--- /dev/null
+++ b/system/javascript/osapjs/vms/filamentSensorVirtualMachine.js
@@ -0,0 +1,68 @@
+/*
+filamentSensorVM.js
+
+vm for filament sensor 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+// import { TS } from '../../osapjs/core/ts.js'
+// import PK from '../../osapjs/core/packets.js'
+
+// export default function FilamentSensorVM(osap, route) {
+
+//   let hallQuery = osap.query(PK.route(route).sib(1).end())
+//   this.getHallReading = () => {
+//     return new Promise((resolve, reject) => {
+//       hallQuery.pull().then((data) => {
+//         let reading = TS.read('float32', data, 0, true)
+//         resolve (reading) 
+//       }).catch((err) => { reject(err) })
+//     })
+//   }
+// }
+
+/*
+filamentSensorVM.js
+
+vm for filament sensor 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import PK from '../../osapjs/core/packets.js'
+
+export default function FilamentSensorVM(osap, route) {
+
+  let query = osap.query(PK.route(route).sib(2).end())
+  this.getReadings = () => {
+    return new Promise((resolve, reject) => {
+      query.pull().then((data) => {
+        let diameter = TS.read('float32', data, 0, true)
+        let posn = TS.read('float32', data, 4, true)
+        let rate = TS.read('float32', data, 8, true)
+        // rate is in encoder-ticks / second ? we should normalize this to our... linear rate 
+        // let count = TS.read('float32', data, 12, true)
+        resolve ({
+          diameter: diameter, 
+          position: posn,
+          rate: rate,
+          // integral: count
+        }) 
+      }).catch((err) => { reject(err) })
+    })
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/loadcellVirtualMachine.js b/system/javascript/osapjs/vms/loadcellVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0489e448ca9f41fd50486318089b71b2cdda539
--- /dev/null
+++ b/system/javascript/osapjs/vms/loadcellVirtualMachine.js
@@ -0,0 +1,136 @@
+/*
+loadcellVirtualMachine.js
+
+vm for loadcell modules 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../core/ts.js'
+import TIME from '../core/time.js'
+import PK from '../core/packets.js'
+import LeastSquares from '../client/utes/lsq.js'
+
+export default function LoadVM(osap, route) {
+  // want a calibration 
+  let lsq = [new LeastSquares(), new LeastSquares(), new LeastSquares()]
+  this.offsets = [0, 0, 0]
+
+  this.setObservations = (units, xy) => {
+    if (units == 'grams') {
+      for (let ch = 0; ch < 3; ch++) {
+        for (let i = 0; i < xy[ch][1].length; i++) {
+          xy[ch][1][i] = xy[ch][1][i] * 0.00980665;
+        }
+        lsq[ch].setObservations(xy[ch])
+        console.log(`lsq[${ch}]: `, lsq[ch].printFunction())
+      }
+    }
+  }
+
+  let readingQuery = osap.query(PK.route(route).sib(2).end())
+  this.getReading = (offset = true, raw = false) => {
+    return new Promise((resolve, reject) => {
+      readingQuery.pull().then((data) => {
+        let readings = [
+          TS.read("int32", data, 0, true),
+          TS.read("int32", data, 4, true),
+          TS.read("int32", data, 8, true)
+        ]
+        let calibrated = [0, 0, 0]
+        let offset = [0, 0, 0]
+        for (let ch = 0; ch < 3; ch++) {
+          calibrated[ch] = lsq[ch].predict(readings[ch])
+          offset[ch] = calibrated[ch] + this.offsets[ch]
+        }
+        if (!offset) {
+          resolve(calibrated)
+        } else if (raw) {
+          resolve(readings)
+        } else {
+          resolve(offset)
+        }
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.tare = () => {
+    return new Promise((resolve, reject) => {
+      this.getReading(false).then((rd) => {
+        for (let ch = 0; ch < 3; ch++) {
+          this.offsets[ch] = -rd[ch]
+        }
+        resolve()
+      }).catch((err) => {
+        reject(err)
+      })
+    })
+  }
+
+  // assuming zero-load when we call this, sets comparator to trigger on next tap 
+  let tapOffset = 0 
+  let tapDir = 0 
+  this.setupTapComparator = async (dir, offset, avgCount = 10) => {
+    try {
+      // we want to collect a sum of reading first, 
+      let sum = 0
+      for (let s = 0; s < avgCount; s++) {
+        let readings = await this.getReading(false, true)
+        await TIME.delay(50)
+        // console.log(readings[0])
+        sum += readings[0]
+      }
+      sum = sum / avgCount
+      console.log(`LC: floating around ${sum}`)
+      // so we want to set the comparator so that... 
+      if (dir == "negative") {
+        dir = 0
+        offset = sum - Math.abs(offset)
+      } else if (dir == "positive") {
+        dir = 1
+        offset = sum + Math.abs(offset)
+      } else {
+        throw new Error("loadcell setup direction ambiguious, pls use 'negative' or 'positive'")
+      }
+      // we can setup now, 
+      tapOffset = offset 
+      tapDir = dir 
+      await this.setComparator(offset, dir)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  // using previous... 
+  this.setupTapForLift = async (bonusOffsetDistance = 100000) => {
+    try {
+      if(tapDir){
+        await this.setComparator(tapOffset + bonusOffsetDistance, tapDir)
+      } else {
+        await this.setComparator(tapOffset - bonusOffsetDistance, tapDir)
+      }
+    } catch (err) {
+      throw err 
+    }
+  }
+
+  // for the comparator... 
+  let cmpEP = osap.endpoint()
+  cmpEP.addRoute(PK.route(route).sib(3).end())
+  this.setComparator = async (val, dir) => {
+    try {
+      let datagram = new Uint8Array(5)
+      datagram[0] = dir ? 1 : 0;
+      TS.write('int32', val, datagram, 1)
+      await cmpEP.write(datagram, "acked")
+    } catch (err) {
+      throw err
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/minimalVirtualMachine.js b/system/javascript/osapjs/vms/minimalVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e68e12bcdd7bbbe7f1d51398bf79037ccfe8649
--- /dev/null
+++ b/system/javascript/osapjs/vms/minimalVirtualMachine.js
@@ -0,0 +1,144 @@
+/*
+minimalVirtualMachine.js
+
+example 'virtual machine' 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import PK from '../../osapjs/core/packets.js'
+import MotionVM from './motionVirtualMachine.js'
+import MotorVM from './motorVirtualMachine.js'
+
+export default function MinVM(osap, headRoute) {
+  // position after-homing:
+  // since we home to top-right, bottom-left is 0,0,0
+  // and we have bounding box then... 
+  let posOnHome = {
+    X: 100,     // about 0->130mm x should be safe,
+    Y: 100,     // about 0->170mm y should be safe
+    Z: 100       // 260mm tall max, abt 
+  }
+
+  // ------------------------------------------------------ MOTION
+  // with base route -> embedded smoothie instance 
+  this.motion = new MotionVM(osap, headRoute)
+
+  // .settings() for rates and accels, 
+  this.motion.settings({
+    accel: {  // mm/sec^2 
+      X: 500,   // 1500
+      Y: 500,   // 1500
+      Z: 250,   // 300, 
+      E: 150    // 500 
+    },
+    maxRate: {  // mm/sec 
+      X: 400,   // 100
+      Y: 400,   // 100 
+      Z: 100,   // 50 
+      E: 50     // 100 
+    },
+    spu: {
+      X: 40, 
+      Y: 40, 
+      Z: 40, 
+      E: 100 
+    }
+  })
+
+  // ------------------------------------------------------ MOTORS
+
+  this.motors = {
+    X: new MotorVM(osap, PK.route(headRoute).sib(1).bfwd(2).end()),
+  }
+
+  // .settings() just preps for the .init() or whatever other call, 
+  this.motors.X.settings({
+    axisPick: 0,
+    axisInversion: false,
+    microstep: 4,         // 
+    currentScale: 0.4,    // 0 -> 1 
+    homeRate: -1000,      // steps / sec 
+    homeOffset: 2000,     // steps 
+  })
+
+  // ------------------------------------------------------ setup / handle motor group
+
+  this.setupMotors = async () => {
+    for (let mot in this.motors) {
+      try {
+        await this.motors[mot].setup()
+      } catch (err) {
+        console.error(`failed to setup ${mot}`)
+        throw err
+      }
+    }
+  }
+
+  this.enableMotors = async () => {
+    for (let mot in this.motors) {
+      try {
+        await this.motors[mot].enable()
+      } catch (err) {
+        console.error(`failed to enable ${mot}`)
+        throw err
+      }
+    }
+  }
+
+  this.disableMotors = async () => {
+    for (let mot in this.motors) {
+      try {
+        await this.motors[mot].disable()
+      } catch (err) {
+        console.error(`failed to disable ${mot}`)
+        throw err
+      }
+    }
+  }
+
+  // ------------------------------------------------------ HOMING 
+
+  this.homeZ = async () => {
+    try {
+      await this.motion.awaitMotionEnd()
+      if (this.motors.Z) {
+        await this.motors.Z.home()
+        await this.motors.Z.awaitHomeComplete()
+      } else {
+        console.warn("on clank.homeZ, no z motor... passing...")
+      }
+    } catch (err) { throw err }
+  }
+
+  this.homeXY = async () => {
+    try {
+      await this.motion.awaitMotionEnd()
+      await this.motors.X.home()
+      await this.motors.X.awaitHomeComplete()
+      /*
+      if (this.motors.X) await this.motors.X.home()
+      if (this.motors.YL) await this.motors.YL.home()
+      if (this.motors.YR) await this.motors.YR.home()
+      if (this.motors.X) await this.motors.X.awaitHomeComplete()
+      if (this.motors.YL) await this.motors.YL.awaitHomeComplete()
+      if (this.motors.YR) await this.motors.YR.awaitHomeComplete()
+      */
+    } catch (err) { throw err }
+  }
+
+  this.home = async () => {
+    try {
+      await this.homeZ()
+      await this.homeXY()
+      await this.motion.setPos(posOnHome)
+    } catch (err) { throw err }
+  }
+
+} // end clank vm 
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/motionVirtualMachine.js b/system/javascript/osapjs/vms/motionVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..79164f36cff2ec6226b66e54b275ef10da5293ae
--- /dev/null
+++ b/system/javascript/osapjs/vms/motionVirtualMachine.js
@@ -0,0 +1,376 @@
+/*
+motionVirtualMachine.js
+
+js handles on embedded smoothieroll 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import PK from '../../osapjs/core/packets.js'
+import { delay } from '../../osapjs/core/time.js'
+
+export default function MotionVM(osap, route) {
+  // ok: we make an 'endpoint' that will transmit moves,
+  let moveEP = osap.endpoint()
+  // add the machine head's route to it, 
+  moveEP.addRoute(PK.route(route).sib(2).end())
+  // and set a long timeout,
+  moveEP.setTimeoutLength(60000)
+  // move like: { position: {X: num, Y: num, Z: num}, rate: num }
+  this.addMoveToQueue = (move) => {
+    // write the gram, 
+    let wptr = 0
+    let datagram = new Uint8Array(20)
+    // write rate 
+    if(rateOverride.state){
+      move.rate = rateOverride.rate 
+    }
+    if(isNaN(move.rate) || isNaN(move.position.X) || isNaN(move.position.Y) || isNaN(move.position.Z)){
+      console.error("NaN in move request")
+      console.log(move)
+      return
+    }
+    console.log(move.rate)
+    wptr += TS.write('float32', move.rate, datagram, wptr, true)
+    // write posns 
+    wptr += TS.write('float32', move.position.X, datagram, wptr, true)
+    wptr += TS.write('float32', move.position.Y, datagram, wptr, true)
+    wptr += TS.write('float32', move.position.Z, datagram, wptr, true)
+    if (move.position.E) {
+      //console.log(move.position.E)
+      //wptr += TS.write('float32', 0, datagram, wptr, true)
+      wptr += TS.write('float32', move.position.E, datagram, wptr, true)
+    } else {
+      wptr += TS.write('float32', 0, datagram, wptr, true)
+    }
+    // do the networking, 
+    return new Promise((resolve, reject) => {
+      moveEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => {
+        reject(err)
+      })
+    })
+  }
+
+  let rateOverride = { state: false, rate: 10 }
+  this.overrideAllRates = (rate) => {
+    console.warn(`MOTION: overriding all rates to ${rate} units/s`)
+    rateOverride = { state: true, rate: rate } 
+  }
+  this.stopRateOverride = () => {
+    rateOverride.state = false 
+  }
+
+  // to set the current position, 
+  let setPosEP = osap.endpoint()
+  setPosEP.addRoute(PK.route(route).sib(3).end())//TS.route().portf(0).portf(1).end(), TS.endpoint(0, 2), 512)
+  setPosEP.setTimeoutLength(10000)
+  this.setPos = (pos) => {
+    let wptr = 0
+    let datagram = new Uint8Array(16)
+    wptr += TS.write('float32', pos.X, datagram, wptr, true)
+    wptr += TS.write('float32', pos.Y, datagram, wptr, true)
+    wptr += TS.write('float32', pos.Z, datagram, wptr, true)
+    if (pos.E) {
+      wptr += TS.write('float32', pos.E, datagram, wptr, true)
+    } else {
+      wptr += TS.write('float32', 0, datagram, wptr, true)
+    }
+    // ship it 
+    return new Promise((resolve, reject) => {
+      setPosEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // an a 'query' to check current position 
+  let posQuery = osap.query(PK.route(route).sib(3).end()) //TS.route().portf(0).portf(1).end(), TS.endpoint(0, 2), 512)
+  this.getPos = () => {
+    return new Promise((resolve, reject) => {
+      posQuery.pull().then((data) => {
+        let pos = {
+          X: TS.read('float32', data, 0, true),
+          Y: TS.read('float32', data, 4, true),
+          Z: TS.read('float32', data, 8, true),
+          E: TS.read('float32', data, 12, true)
+        }
+        resolve(pos)
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // query for (time of query) speeds 
+  let vQuery = osap.query(PK.route(route).sib(4).end())
+  this.getSpeeds = () => {
+    return new Promise((resolve, reject) => {
+      vQuery.pull().then((data) => {
+        let speeds = {
+          X: TS.read('float32', data, 0, true),
+          Y: TS.read('float32', data, 4, true),
+          Z: TS.read('float32', data, 8, true),
+          E: TS.read('float32', data, 12, true)
+        }
+        resolve(speeds)
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // another query to see if it's currently moving, 
+  // update that endpoint so we can 'write halt' / 'write go' with a set 
+  let motionQuery = osap.query(PK.route(route).sib(5).end())//TS.route().portf(0).portf(1).end(), TS.endpoint(0, 3), 512)
+  this.getMotionState = () => {
+    return new Promise((resolve, reject) => {
+      motionQuery.pull().then((data) => {
+        if (data[0] > 0) {
+          resolve(true)
+        } else {
+          resolve(false)
+        }
+      }).catch((err) => {
+        reject(err)
+      })
+    })
+  }
+
+  this.awaitMotionEnd = () => {
+    return new Promise((resolve, reject) => {
+      let check = () => {
+        motionQuery.pull().then((data) => {
+          if (data[0] > 0) {
+            setTimeout(check, 50)
+          } else {
+            resolve()
+          }
+        }).catch((err) => {
+          reject(err)
+        })
+      }
+      setTimeout(check, 50)
+    })
+  }
+
+  // an endpoint to write 'wait time' on the remote,
+  let waitTimeEP = osap.endpoint()
+  waitTimeEP.addRoute(PK.route(route).sib(6).end())//TS.route().portf(0).portf(1).end(), TS.endpoint(0, 4), 512)
+  this.setWaitTime = (ms) => {
+    return new Promise((resolve, reject) => {
+      let datagram = new Uint8Array(4)
+      TS.write('uint32', ms, datagram, 0, true)
+      waitTimeEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.delta = async (move, rate, awaitCompletion = true) => {
+    try {
+      if (!rate) rate = 6000
+      await this.setWaitTime(1)
+      await delay(5)
+      await this.awaitMotionEnd()
+      let cp = await this.getPos()
+      await this.addMoveToQueue({
+        position: { X: cp.X + move[0], Y: cp.Y + move[1], Z: cp.Z + move[2] },
+        rate: rate
+      })
+      if(awaitCompletion){
+        await delay(5)
+        await this.awaitMotionEnd()
+        await this.setWaitTime(100)  
+      }
+    } catch (err) {
+      console.error('arising during delta')
+      throw err
+    }
+  }
+
+  // for spot moves... stub, should do if no info (missing rate, missing axis) fills in with current 
+  this.goTo = async (move) => {
+    try {
+      await this.awaitMotionEnd()
+      await this.setWaitTime(10)
+      await this.addMoveToQueue(move)
+      await delay(5)
+      await this.awaitMotionEnd()
+      await this.setWaitTime(1000)
+    } catch (err) { throw err }
+  }
+
+  // endpoint to set per-axis accelerations,
+  let accelEP = osap.endpoint()
+  accelEP.addRoute(PK.route(route).sib(7).end())
+  this.setAccels = (accels) => {
+    // mm/sec/sec 
+    let wptr = 0
+    let datagram = new Uint8Array(16)
+    wptr += TS.write('float32', accels.X, datagram, wptr, true)
+    wptr += TS.write('float32', accels.Y, datagram, wptr, true)
+    wptr += TS.write('float32', accels.Z, datagram, wptr, true)
+    if (accels.E) {
+      wptr += TS.write('float32', accels.E, datagram, wptr, true)
+    } else {
+      wptr += TS.write('float32', 0, datagram, wptr, true)
+    }
+    return new Promise((resolve, reject) => {
+      accelEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  let rateEP = osap.endpoint()
+  rateEP.addRoute(PK.route(route).sib(8).end())
+  this.setMaxRates = (rates) => {
+    // mm / sec 
+    let wptr = 0
+    let datagram = new Uint8Array(16)
+    wptr += TS.write('float32', rates.X, datagram, wptr, true)
+    wptr += TS.write('float32', rates.Y, datagram, wptr, true)
+    wptr += TS.write('float32', rates.Z, datagram, wptr, true)
+    if (rates.E) {
+      wptr += TS.write('float32', rates.E, datagram, wptr, true)
+    } else {
+      wptr += TS.write('float32', 100, datagram, wptr, true)
+    }
+    return new Promise((resolve, reject) => {
+      rateEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  let spuEP = osap.endpoint()
+  spuEP.addRoute(PK.route(route).sib(9).end())
+  this.setSPUs = (spus) => {
+    // steps / unit 
+    let wptr = 0
+    let datagram = new Uint8Array(16)
+    wptr += TS.write('float32', spus.X, datagram, wptr, true)
+    wptr += TS.write('float32', spus.Y, datagram, wptr, true)
+    wptr += TS.write('float32', spus.Z, datagram, wptr, true)
+    wptr += TS.write('float32', spus.E, datagram, wptr, true)
+    return new Promise((resolve, reject) => {
+      spuEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  let powerEP = osap.endpoint() 
+  powerEP.addRoute(PK.route(route).sib(10).end())
+  let powerQuery = osap.query(PK.route(route).sib(10).end())
+
+  this.setPowerStates = (v5, v24) => {
+    // 5v on / off, 24v on / off, 
+    let wptr = 0;
+    let datagram = new Uint8Array(2)
+    wptr += TS.write('boolean', v5, datagram, wptr, true)
+    wptr += TS.write('boolean', v24, datagram, wptr, true)
+    return new Promise((resolve, reject) => {
+      powerEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.getPowerStates = () => {
+    return new Promise((resolve, reject) => {
+      powerQuery.pull().then((data) => {
+        resolve(data) 
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // ------------------------------------------------------ JS API 
+
+  this.config = {
+    accel: { // mm/sec/sec
+      X: 1000,
+      Y: 1000,
+      Z: 1000,
+      E: 1000
+    },
+    maxRate: {  // mm/sec 
+      X: 100,
+      Y: 100,
+      Z: 100,
+      E: 100
+    },
+    spu: {
+      X: 40,
+      Y: 40,
+      Z: 40, 
+      E: 40
+    }
+  }
+
+  this.settings = (settings) => {
+    // this could be a proper forever-recursion to diff against 
+    // default config setup... however;
+    for (let key in settings) {
+      if (key == 'accel' && key in this.config) {
+        for (let axis in settings.accel) {
+          if (axis in this.config.accel) {
+            this.config.accel[axis] = settings.accel[axis]
+          } else {
+            console.warn(`motion/accel settings spec axis '${axis}', it doesn't exist`)
+          }
+        }
+      } else if (key == 'maxRate' && key in this.config) {
+        for (let axis in settings.maxRate) {
+          if (axis in this.config.maxRate) {
+            this.config.maxRate[axis] = settings.maxRate[axis]
+          } else {
+            console.warn(`motion/maxRate settings spec axis '${axis}', it doesn't exist`)
+          }
+        }
+      } else if (key == 'spu' && key in this.config) {
+        for(let axis in settings.spu){
+          if(axis in this.config.spu){
+            this.config.spu[axis] = settings.spu[axis]
+          } else {
+            console.warn(`motion/spu settings spec axis '${axis}', it doesn't exist`)
+          }
+        }
+      } else {
+        console.warn(`motion settings spec key '${key}', it doesn't exist!`)
+      }
+    }
+    // watch for bad keys ? 
+  }
+
+  this.setup = async () => {
+    try {
+      await this.awaitMotionEnd()
+      await this.setAccels(this.config.accel)
+      await this.setMaxRates(this.config.maxRate)
+      await this.setSPUs(this.config.spu)
+    } catch (err) {
+      throw err
+    }
+  }
+
+  this.setZ = async (zPos) => {
+    try {
+      await this.awaitMotionEnd()
+      let cPos = await this.getPos()
+      console.log(cPos)
+      cPos.Z = zPos // just update one 
+      //cPos.X = 65
+      //cPos.Y = 85
+      await this.setPos(cPos)
+    } catch (err) {
+      throw err
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/motorVirtualMachine.js b/system/javascript/osapjs/vms/motorVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc04249c3340c88602da81663548240514346125
--- /dev/null
+++ b/system/javascript/osapjs/vms/motorVirtualMachine.js
@@ -0,0 +1,186 @@
+/*
+tempVirtualMachine.js
+
+vm for stepper motors 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import PK from '../../osapjs/core/packets.js'
+
+export default function MotorVM(osap, route) {
+
+  // ------------------------------------------------------ OSAP Interfaces, 
+
+  // 0: usb interface
+  // 1: bus interface 
+
+  // -------------------------------------------- 2: axis pick 
+  let axisPickEP = osap.endpoint()
+  axisPickEP.addRoute(PK.route(route).sib(2).end())
+  this.setAxisPick = (pick) => {
+    let datagram = new Uint8Array(1)
+    TS.write('uint8', pick, datagram, 0, true)
+    return new Promise((resolve, reject) => {
+      axisPickEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })  
+    })
+  }
+  
+  // -------------------------------------------- 3: axis inversion 
+  let axisInvertEP = osap.endpoint()
+  axisInvertEP.addRoute(PK.route(route).sib(3).end())
+  this.setAxisInversion = (invert) => {
+    let datagram = new Uint8Array(1)
+    TS.write('boolean', invert, datagram, 0, true)
+    return new Promise((resolve, reject) => {
+      axisInvertEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })  
+    })
+  }
+
+  // -------------------------------------------- 4: microstep 
+  let microstepEP = osap.endpoint()
+  microstepEP.addRoute(PK.route(route).sib(4).end())
+  this.setMicrostep = (micro) => {
+    let datagram = new Uint8Array(1)
+    datagram[0] = micro 
+    //console.log(datagram[0])
+    return new Promise((resolve, reject) => {
+      microstepEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // -------------------------------------------- 6: active current scaling 
+  let cscaleEP = osap.endpoint()
+  cscaleEP.addRoute(PK.route(route).sib(5).end())
+  this.setCScale = (cscale) => {
+    let datagram = new Uint8Array(4)
+    TS.write('float32', cscale, datagram, 0, true)
+    return new Promise((resolve, reject) => {
+      cscaleEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })  
+    })
+  }
+
+  // -------------------------------------------- 7: homing 
+  let homeEP = osap.endpoint()
+  homeEP.addRoute(PK.route(route).sib(6).end())
+  this.home = () => {
+    let rate = this.config.homeRate
+    let offset = this.config.homeOffset
+    let datagram = new Uint8Array(8)
+    TS.write('int32', rate, datagram, 0, true)
+    TS.write('uint32', offset, datagram, 4, true)
+    return new Promise((resolve, reject) => {
+      homeEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // -------------------------------------------- 8: homing state 
+  let homeStateQuery = osap.query(PK.route(route).sib(7).end())
+  this.getHomeState = () => {
+    return new Promise((resolve, reject) => {
+      homeStateQuery.pull().then((data) => {
+        if(data[0] > 0){
+          resolve(true)
+        } else {
+          resolve(false)
+        }
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.awaitHomeComplete = () => {
+    return new Promise((resolve, reject) => {
+      let check = () => {
+        this.getHomeState().then((homing) => {
+          if(homing){
+            setTimeout(check, 50)
+          } else {
+            resolve()
+          }
+        }).catch((err) => { reject(err) })
+      } // end 'check' def 
+      setTimeout(check, 50)
+    })
+  }
+
+  // ------------------------------------------------------ JS API
+
+  // default config, 
+  this.config = {
+    axisPick: 0, 
+    axisInversion: false, 
+    microstep: 16, // 1, 4, 8, 16, 32, 64 
+    currentScale: 0.2,
+    homeRate: 10,
+    homeOffset: 10
+  }
+
+  // update config 
+  this.settings = (settings, publish) => {
+    // could do: on each setup change, if flag 'publish' set, do network work here 
+    // would mean this becomes async... 
+    // also, check microstep is allowed 
+    if(settings.microstep){
+      switch(settings.microstep){
+        case 1:
+        case 4:
+        case 8:
+        case 16:
+        case 32:
+        case 64:
+          break;
+        default:
+          throw new Error("microstep motor setting must be 1 or power of 2, max 64")
+          break;
+      }
+    }
+    for(let key in settings){
+      if(key in this.config){
+        this.config[key] = settings[key]
+      } else {
+        console.warn(`motor settings spec key '${key}', it doesn't exist!`)
+      }
+    }
+  }
+
+  // publish settings to motors 
+  this.setup = async () => {
+    try {
+      await this.setAxisPick(this.config.axisPick)
+      await this.setAxisInversion(this.config.axisInversion)
+      await this.setMicrostep(this.config.microstep)
+      await this.setCScale(0.0) // default: off 
+    } catch (err) { throw err }
+  }
+
+  // enable the thing,
+  this.enable = async () => {
+    try {
+      await this.setCScale(this.config.currentScale)
+    } catch (err) { throw err }
+  }
+
+  this.disable = async () => {
+    try {
+      await this.setCScale(0.0)
+    } catch (err) { throw err }
+  }
+
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/powerSwitches.js b/system/javascript/osapjs/vms/powerSwitches.js
new file mode 100644
index 0000000000000000000000000000000000000000..e02568cae7c6fd1fa28e613a90d6f04586c16cc2
--- /dev/null
+++ b/system/javascript/osapjs/vms/powerSwitches.js
@@ -0,0 +1,62 @@
+/*
+powerSwitchesVM
+
+quick access to any 'powerSwitches' endpoint, 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../core/ts.js'
+import PK from '../core/packets.js'
+import AXLMotionVM from './axlMotionVM.js'
+
+export default function PowerSwitchVM(osap) {
+
+  // and some bonus power-switching capability... 
+  let powerEP = osap.endpoint("powerMirror") 
+  let powerQuery = {} 
+  // powerEP.addRoute(PK.route(route).sib(6).end())
+  // let powerQuery = osap.query(PK.route(route).sib(6).end())
+
+  this.setPowerStates = (v5, v24) => {
+    // 5v on / off, 24v on / off, 
+    let wptr = 0;
+    let datagram = new Uint8Array(2)
+    wptr += TS.write('boolean', v5, datagram, wptr, true)
+    wptr += TS.write('boolean', v24, datagram, wptr, true)
+    return new Promise((resolve, reject) => {
+      powerEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  this.getPowerStates = () => {
+    return new Promise((resolve, reject) => {
+      powerQuery.pull().then((data) => {
+        resolve(data) 
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // ------------------------------------------------------ SETUP 
+  this.setup = async () => {
+    try {
+      // find it... 
+      let route = (await osap.nr.find("ep_powerSwitches")).route
+      // erp-derp, confused route api alert, 
+      route = PK.VC2EPRoute(route)
+      powerEP.addRoute(route)
+      powerQuery = osap.query(route)
+    } catch (err) {
+      console.error(err)
+      throw err 
+    }
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/quantickVirtualMachine.js b/system/javascript/osapjs/vms/quantickVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..4064f158f769b57c1ec9587a096946d415daeb0e
--- /dev/null
+++ b/system/javascript/osapjs/vms/quantickVirtualMachine.js
@@ -0,0 +1,142 @@
+/*
+quantickVirtalMachine.js
+
+vm for smart motors  
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../../osapjs/core/ts.js'
+import TIME from '../../osapjs/core/time.js'
+import PK from '../../osapjs/core/packets.js'
+
+export default function QuantickVM(osap, route) {
+  // one to get states, 
+  let stateQuery = osap.query(PK.route(route).sib(2).end())
+  this.getStates = () => {
+    return new Promise((resolve, reject) => {
+      stateQuery.pull().then((data) => {
+        let pos = TS.read('float32', data, 0)
+        resolve({
+          pos_est: pos
+        })
+      }).catch((err) => { reject (err) })
+    })
+  }
+  // one to set modes & one to query modes, 
+  // kick calib 
+  let modeEP = osap.endpoint()
+  modeEP.addRoute(PK.route(route).sib(3).end())
+  let modeQuery = osap.query(PK.route(route).sib(3).end())
+  // possible modes, 
+  let QTMODES = { "disabled": 0, "calibrating": 1, "enabled": 2 }
+  this.setMode = (mode) => {
+    return new Promise((resolve, reject) => {
+      if (QTMODES[mode]) {
+        let datagram = new Uint8Array(1)
+        datagram[0] = QTMODES[mode]
+        modeEP.write(datagram, "acked").then(() => {
+          resolve()
+        }).catch((err) => { reject(err) })
+      } else {
+        reject(`mode ${mode} doesn't exist`)
+      }
+    })
+  }
+
+  this.getMode = () => {
+    return new Promise((resolve, reject) => {
+      modeQuery.pull().then((mode) => {
+        // yikes 
+        switch (mode[0]) {
+          case 0:
+            resolve("disabled")
+            break;
+          case 1:
+            resolve("calibrating")
+            break;
+          case 2:
+            resolve("enabled")
+            break;
+          default:
+            reject(`broken mode ${mode}`);
+            break;
+        }
+      }).catch((err) => reject(err))
+    })
+  }
+
+  let encoderMapQ = osap.queryMSeg(PK.route(route).sib(4).end())
+  this.getEncoderMap = async () => {
+    return new Promise((resolve, reject) => {
+      encoderMapQ.pull().then((data) => {
+        // data is in float... use view 
+        let uint8 = new Uint8Array(data)
+        let floats = new Float32Array(uint8.buffer)
+        resolve(floats)
+      }).catch((err) => { reject(err) })  
+    })
+  }
+
+  this.runCalib = async () => {
+    try {
+      await this.setMode("calibrating")
+      let startTime = TIME.getTimeStamp()
+      while (true) {
+        let mode = await this.getMode()
+        ///console.log(`${mode}...`)
+        if (mode != "calibrating") {
+          console.log(`calibrating done, mode: ${mode}`)
+          break;
+        }
+        // if (startTime + 50000 < TIME.getTimeStamp()) {
+        //   console.log("calibration timeout!")
+        //   break;
+        // }
+        await TIME.delay(100)
+      }
+      //let map = await this.getEncoderMap()
+      //return map
+    } catch (err) {
+      console.error(err)
+    }
+  }
+  // mseg / calibration 
+  /*
+  
+  // stacked up states 
+  let pulseCountQuery = osap.query(PK.route(route).sib(3).end())
+  this.getPulseCount = () => {
+    return new Promise((resolve, reject) => {
+      pulseCountQuery.pull().then((data) => {
+        let count = TS.read('uint32', data, 0)
+        let pulseWidth = TS.read('float32', data, 4)
+        let mpErrCount = TS.read('uint32', data, 8)
+        let idxErrCount = TS.read('uint32', data, 12)
+        let qErrCount = TS.read('uint32', data, 16)
+        resolve([count, pulseWidth, mpErrCount, idxErrCount, qErrCount])
+      }).catch((err) => { reject (err) })
+    })
+  }
+  // make torque requests 
+  let torqueRequestEP = osap.endpoint()
+  torqueRequestEP.addRoute(PK.route(route).sib(4).end())
+  this.setTorque = (flt) => {
+    return new Promise((resolve, reject) => {
+      // -1.0 -> 1.0, embedded will clamp 
+      let wptr = 0
+      let datagram = new Uint8Array(4)
+      wptr += TS.write('float32', flt, datagram, wptr)
+      torqueRequestEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+  */
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vms/tempVirtualMachine.js b/system/javascript/osapjs/vms/tempVirtualMachine.js
new file mode 100644
index 0000000000000000000000000000000000000000..57b61999a75986c7cc8c8b7ba8cdc8d0210f3cca
--- /dev/null
+++ b/system/javascript/osapjs/vms/tempVirtualMachine.js
@@ -0,0 +1,100 @@
+/*
+tempVirtualMachine.js
+
+vm for heater modules 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2021
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { TS } from '../core/ts.js'
+import TIME from '../core/time.js'
+import PK from '../core/packets.js'
+
+export default function TempVM(osap, route) {
+  // set a temp 
+  let tempSetEP = osap.endpoint()
+  tempSetEP.addRoute(PK.route(route).sib(2).end())
+  this.setExtruderTemp = (temp) => {
+    return new Promise((resolve, reject) => {
+      let datagram = new Uint8Array(4)
+      TS.write('float32', temp, datagram, 0, true)
+      tempSetEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // query current temp 
+  let tempQuery = osap.query(PK.route(route).sib(3).end(), 3)
+  this.getExtruderTemp = () => {
+    return new Promise((resolve, reject) => {
+      tempQuery.pull().then((data) => {
+        let temp = TS.read('float32', data, 0, true)
+        resolve(temp)
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // await temp...
+  this.awaitExtruderTemp = async (temp) => {
+    try {
+      await this.setExtruderTemp(temp)
+      while(true){
+        await TIME.delay(250)
+        let ct = await this.getExtruderTemp()
+        // console.log(`temp: ${ct}`)
+        if(temp + 1 > ct && temp - 1 < ct){
+          console.log('temp OK')
+          break 
+        }
+      }
+    } catch (err) {
+      throw err 
+    }
+  }
+
+  // query current heater effort 
+  let outputQuery = osap.query(PK.route(route).sib(4).end())
+  this.getExtruderTempOutput = () => {
+    return new Promise((resolve, reject) => {
+      outputQuery.pull().then((data) => {
+        let effort = TS.read('float32', data, 0, true)
+        resolve(effort)
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // set PID terms 
+  let tempPIDTermsEP = osap.endpoint()
+  tempPIDTermsEP.addRoute(PK.route(route).sib(5).end())
+  this.setPIDTerms = (vals) => {
+    return new Promise((resolve, reject) => {
+      let datagram = new Uint8Array(12)
+      TS.write('float32', vals[0], datagram, 0, true)
+      TS.write('float32', vals[1], datagram, 4, true)
+      TS.write('float32', vals[2], datagram, 8, true)
+      tempPIDTermsEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+
+  // set PCF ratio 
+  let pcfEP = osap.endpoint()
+  pcfEP.addRoute(PK.route(route).sib(6).end())
+  this.setPCF = (duty) => {
+    return new Promise((resolve, reject) => {
+      let datagram = new Uint8Array(4)
+      TS.write('float32', duty, datagram, 0, true)
+      pcfEP.write(datagram, "acked").then(() => {
+        resolve()
+      }).catch((err) => { reject(err) })
+    })
+  }
+}
\ No newline at end of file
diff --git a/system/javascript/osapjs/vport/vPortSerial.js b/system/javascript/osapjs/vport/vPortSerial.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a6e9663188c1c4c008c727572f3fc05a78b5aca
--- /dev/null
+++ b/system/javascript/osapjs/vport/vPortSerial.js
@@ -0,0 +1,181 @@
+/*
+vPortSerial.js
+
+link layer 
+
+Jake Read at the Center for Bits and Atoms
+(c) Massachusetts Institute of Technology 2022
+
+This work may be reproduced, modified, distributed, performed, and
+displayed for any purpose, but must acknowledge the open systems assembly protocol (OSAP) project.
+Copyright is retained and must be preserved. The work is provided as is;
+no warranty is provided, and users accept all liability.
+*/
+
+import { SerialPort, DelimiterParser } from 'serialport'
+import { TS } from '../core/ts.js'
+import TIME from '../core/time.js'
+import COBS from "../utes/cobs.js"
+
+// have some "protocol" at the link layer 
+// buffer is max 256 long for that sweet sweet uint8_t alignment 
+let SERLINK_BUFSIZE = 255
+// -1 checksum, -1 packet id, -1 packet type, -2 cobs
+let SERLINK_SEGSIZE = SERLINK_BUFSIZE - 5
+// packet keys; 
+let SERLINK_KEY_PCK = 170  // 0b10101010
+let SERLINK_KEY_ACK = 171  // 0b10101011
+let SERLINK_KEY_DBG = 172
+let SERLINK_KEY_KEEPALIVE = 173 // keepalive ping 
+// retry settings 
+let SERLINK_RETRY_MACOUNT = 2
+let SERLINK_RETRY_TIME = 100  // milliseconds  
+let SERLINK_KEEPALIVE_TX_TIME = 800 // ms, dead-time interval before sending an 'i'm-still-here' ping 
+let SERLINK_KEEPALIVE_RX_TIME = 1200 // ms, dead-time interval before assuming neighbour is dead 
+
+export default function VPortSerial(osap, portName, debug = false) {
+  // track la, 
+  this.portName = portName
+  // make the vport object (will auto attach to osap)
+  let vport = osap.vPort(`vport_${this.portName}`)
+  vport.maxSegLength = 255
+  // open the port itself, 
+  if (debug) console.log(`SERPORT contact at ${this.portName}, opening`)
+  // we keep a little state, as a treat 
+  let outAwaiting = null
+  let outAwaitingId = 1
+  let outAwaitingTimer = null
+  let numRetries = 0
+  let lastIdRxd = 0
+  let lastRxTime = 0 // last time we heard back ? for keepalive 
+  // flowcontrol is based on this state, 
+  this.status = "opening"
+  let flowCondition = () => {
+    return (outAwaiting == null)
+  }
+  // we report flowcondition, 
+  vport.cts = () => { return (this.status == "open" && flowCondition()) }
+  // and open / closed-ness, 
+  vport.isOpen = () => { return (this.status == "open" && (TIME.getTimeStamp() - lastRxTime) < SERLINK_KEEPALIVE_RX_TIME) }
+  // we have a port... 
+  let port = new SerialPort({
+    path: this.portName,
+    baudRate: 9600
+  })
+  port.on('open', () => {
+    // we track remote open spaces, this is stateful per link... 
+    console.log(`SERPORT at ${this.portName} OPEN`)
+    // is now open,
+    this.status = "open"
+    // we do some keepalive, 
+    let keepAliveTimer = null
+    // we can manually write this, 
+    let keepAlivePacket = new Uint8Array([3, SERLINK_KEY_KEEPALIVE, 0])
+    // clear current keepAlive timer and set new one, 
+    let keepAliveTxUpdate = () => {
+      if (keepAliveTimer) { clearTimeout(keepAliveTimer) }
+      keepAliveTimer = setTimeout(() => {
+        port.write(keepAlivePacket)
+        keepAliveTxUpdate()
+      }, SERLINK_KEEPALIVE_TX_TIME)
+    }
+    // also set last-rx to now, and init keepalive state, 
+    lastRxTime = TIME.getTimeStamp()
+    keepAliveTxUpdate()
+    // to get, use delimiter
+    let parser = port.pipe(new DelimiterParser({ delimiter: [0] }))
+    //let parser = port.pipe(new ByteLength({ length: 1 }))
+    // implement rx
+    parser.on('data', (buf) => {
+      lastRxTime = TIME.getTimeStamp()
+      if (debug) console.log('SERPORT Rx', buf)
+      // checksum... 
+      if (buf.length + 1 != buf[0]) {
+        console.log(`SERPORT Rx Bad Checksum, ${buf[0]} reported, ${buf.length} received`)
+        return
+      }
+      // ack / pack: check and clear, or noop 
+      switch (buf[1]) {
+        case SERLINK_KEY_ACK:
+          if (buf[2] == outAwaitingId) outAwaiting = null;
+          break;
+        case SERLINK_KEY_PCK:
+          if (buf[2] == lastIdRxd) {
+            console.log(`SERPORT Rx double id ${buf[2]}`)
+            return
+          } else {
+            lastIdRxd = buf[2]
+            let decoded = COBS.decode(buf.slice(3))
+            vport.awaitStackAvailableSpace(0, 2000).then(() => {
+              //console.log('SERPORT RX Decoded', decoded)
+              vport.receive(decoded)
+              // output an ack, 
+              let ack = new Uint8Array(4)
+              ack[0] = 4
+              ack[1] = SERLINK_KEY_ACK
+              ack[2] = lastIdRxd
+              ack[3] = 0
+              port.write(ack)
+            })
+          }
+          break;
+        case SERLINK_KEY_DBG:
+          {
+            let decoded = COBS.decode(buf.slice(2))
+            let str = TS.read('string', decoded, 0, true).value; console.log("LL: ", str)
+          }
+          break;
+        case SERLINK_KEY_KEEPALIVE:
+          // this is just for updating lastRxTime... 
+          break;
+        default:
+          console.error(`SERPORT Rx unknown front-key ${buf[1]}`)
+          break;
+      }
+    })
+    // implement tx
+    vport.send = (buffer) => {
+      // double guard, idk
+      if (!flowCondition()) return;
+      // buffers, uint8arrays, all the same afaik 
+      // we are len + cobs start + cobs delimit + pck/ack + id + checksum ? 
+      outAwaiting = new Uint8Array(buffer.length + 5)
+      outAwaiting[0] = buffer.length + 5
+      outAwaiting[1] = SERLINK_KEY_PCK
+      outAwaitingId++; if (outAwaitingId > 255) outAwaitingId = 1;
+      outAwaiting[2] = outAwaitingId
+      outAwaiting.set(COBS.encode(buffer), 3)
+      // reset retry states 
+      clearTimeout(outAwaitingTimer)
+      numRetries = 0
+      // ship eeeet 
+      if (debug) console.log('SERPORT Tx', outAwaiting)
+      port.write(outAwaiting)
+      keepAliveTxUpdate()
+      // retry timeout, in reality USB is robust enough, but codes sometimes bungle messages too 
+      outAwaitingTimer = setTimeout(() => {
+        if (outAwaiting && numRetries < SERLINK_RETRY_MACOUNT && port.isOpen) {
+          port.write(outAwaiting)
+          keepAliveTxUpdate()
+          numRetries++
+        } else if (!outAwaiting) {
+          // noop
+        } else {
+          // cancel 
+          outAwaiting = null
+        }
+      }, SERLINK_RETRY_TIME)
+    }
+  }) // end on-open
+  // close on errors, 
+  port.on('error', (err) => {
+    this.status = "closing"
+    console.log(`SERPORT ${this.portName} ERR`, err)
+    if (port.isOpen) port.close()
+  })
+  port.on('close', (evt) => {
+    vport.dissolve()
+    console.log(`SERPORT ${this.portName} closed`)
+    this.status = "closed"
+  })
+}
\ No newline at end of file
diff --git a/system/javascript/package-lock.json b/system/javascript/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..6b4ce02aa400a34b8287ec2315df3f620c9ee2ba
--- /dev/null
+++ b/system/javascript/package-lock.json
@@ -0,0 +1,1413 @@
+{
+  "name": "minimal-controller",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "minimal-controller",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "esm": "^3.2.25",
+        "express": "^4.17.2",
+        "serialport": "^10.4.0",
+        "ws": "^8.4.0"
+      }
+    },
+    "node_modules/@serialport/binding-mock": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
+      "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
+      "dependencies": {
+        "@serialport/bindings-interface": "^1.2.1",
+        "debug": "^4.3.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/@serialport/binding-mock/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@serialport/binding-mock/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/@serialport/bindings-cpp": {
+      "version": "10.7.0",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.7.0.tgz",
+      "integrity": "sha512-Xx1wA2UCG2loS32hxNvWJI4smCzGKhWqE85//fLRzHoGgE1lSLe3Nk7W40/ebrlGFHWRbQZmeaIF4chb2XLliA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@serialport/bindings-interface": "1.2.1",
+        "@serialport/parser-readline": "^10.2.1",
+        "debug": "^4.3.2",
+        "node-addon-api": "^4.3.0",
+        "node-gyp-build": "^4.3.0"
+      },
+      "engines": {
+        "node": ">=12.17.0 <13.0 || >=14.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/bindings-cpp/node_modules/@serialport/bindings-interface": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.1.tgz",
+      "integrity": "sha512-63Dyqz2gtryRDDckFusOYqLYhR3Hq/M4sEdbF9i/VsvDb6T+tNVgoAKUZ+FMrXXKnCSu+hYbk+MTc0XQANszxw==",
+      "engines": {
+        "node": "^12.22 || ^14.13 || >=16"
+      }
+    },
+    "node_modules/@serialport/bindings-cpp/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@serialport/bindings-cpp/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/@serialport/bindings-interface": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
+      "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
+      "engines": {
+        "node": "^12.22 || ^14.13 || >=16"
+      }
+    },
+    "node_modules/@serialport/parser-byte-length": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.3.0.tgz",
+      "integrity": "sha512-pJ/VoFemzKRRNDHLhFfPThwP40QrGaEnm9TtwL7o2GihEPwzBg3T0bN13ew5TpbbUYZdMpUtpm3CGfl6av9rUQ==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-cctalk": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.3.0.tgz",
+      "integrity": "sha512-8ujmk8EvVbDPrNF4mM33bWvUYJOZ0wXbY3WCRazHRWvyCdL0VO0DQvW81ZqgoTpiDQZm5r8wQu9rmuemahF6vQ==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-delimiter": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.3.0.tgz",
+      "integrity": "sha512-9E4Vj6s0UbbcCCTclwegHGPYjJhdm9qLCS0lowXQDEQC5naZnbsELemMHs93nD9jHPcyx1B4oXkMnVZLxX5TYw==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-inter-byte-timeout": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.3.0.tgz",
+      "integrity": "sha512-wKP0QK85NHgvT6BBB1qBfKBBU4pf8kespNXAZBUYmFT+P4n8r8IZE2mqigCD+AiZcfWNQoAizwOsT/Jx/qeVig==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-packet-length": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.3.0.tgz",
+      "integrity": "sha512-bj0cWzt8YSQj/E5fRQVYdi4TsfTlZQrXlXrUwjyTsCONv8IPOHzsz+yY0fw5SEMiJtaLyqvPkCHLsttOd/zFsg==",
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/@serialport/parser-readline": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.3.0.tgz",
+      "integrity": "sha512-ki3ATZ3/RAqnqGROBKE7k+OeZ0DZXZ53GTca4q71OU5RazbbNhTOBQLKLXD3v9QZXCMJdg4hGW/2Y0DuMUqMQg==",
+      "dependencies": {
+        "@serialport/parser-delimiter": "10.3.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-ready": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.3.0.tgz",
+      "integrity": "sha512-1owywJ4p592dJyVrEJZPIh6pUZ3/y/LN6kGTDH2wxdewRUITo/sGvDy0er5i2+dJD3yuowiAz0dOHSdz8tevJA==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-regex": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.3.0.tgz",
+      "integrity": "sha512-tIogTs7CvTH+UUFnsvE7i33MSISyTPTGPWlglWYH2/5coipXY503jlaYS1YGe818wWNcSx6YAjMZRdhTWwM39w==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-slip-encoder": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.3.0.tgz",
+      "integrity": "sha512-JI0ILF5sylWn8f0MuMzHFBix/iMUTa79/Z95KaPZYnVaEdA7h7hh/o21Jmon/26P3RJwL1SNJCjZ81zfan+LtQ==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/parser-spacepacket": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.3.0.tgz",
+      "integrity": "sha512-PDF73ClEPsClD1FEJZHNuBevDKsJCkqy/XD5+S5eA6+tY5D4HLrVgSWsg+3qqB6+dlpwf2CzHe+uO8D3teuKHA==",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/stream": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.3.0.tgz",
+      "integrity": "sha512-7sooi5fHogYNVEJwxVdg872xO6TuMgQd2E9iRmv+o8pk/1dbBnPkmH6Ka3st1mVE+0KnIJqVlgei+ncSsqXIGw==",
+      "dependencies": {
+        "@serialport/bindings-interface": "1.2.1",
+        "debug": "^4.3.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/@serialport/stream/node_modules/@serialport/bindings-interface": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.1.tgz",
+      "integrity": "sha512-63Dyqz2gtryRDDckFusOYqLYhR3Hq/M4sEdbF9i/VsvDb6T+tNVgoAKUZ+FMrXXKnCSu+hYbk+MTc0XQANszxw==",
+      "engines": {
+        "node": "^12.22 || ^14.13 || >=16"
+      }
+    },
+    "node_modules/@serialport/stream/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@serialport/stream/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "dependencies": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "node_modules/body-parser": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
+      "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
+      "dependencies": {
+        "bytes": "3.1.1",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.9.6",
+        "raw-body": "2.4.2",
+        "type-is": "~1.6.18"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
+      "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "node_modules/esm": {
+      "version": "3.2.25",
+      "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+      "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
+      "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
+      "dependencies": {
+        "accepts": "~1.3.7",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.19.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.1.2",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.9.6",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.17.2",
+        "serve-static": "1.14.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.34",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+      "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+      "dependencies": {
+        "mime-db": "1.51.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+      "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",
+      "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.9.6",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
+      "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==",
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
+      "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
+      "dependencies": {
+        "bytes": "3.1.1",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/send": {
+      "version": "0.17.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+      "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.8.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/serialport": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/serialport/-/serialport-10.4.0.tgz",
+      "integrity": "sha512-PszPM5SnFMgSXom60PkKS2A9nMlNbHkuoyRBlzdSWw9rmgOn258+V0dYbWMrETJMM+TJV32vqBzjg5MmmUMwMw==",
+      "dependencies": {
+        "@serialport/binding-mock": "10.2.2",
+        "@serialport/bindings-cpp": "10.7.0",
+        "@serialport/parser-byte-length": "10.3.0",
+        "@serialport/parser-cctalk": "10.3.0",
+        "@serialport/parser-delimiter": "10.3.0",
+        "@serialport/parser-inter-byte-timeout": "10.3.0",
+        "@serialport/parser-packet-length": "10.3.0",
+        "@serialport/parser-readline": "10.3.0",
+        "@serialport/parser-ready": "10.3.0",
+        "@serialport/parser-regex": "10.3.0",
+        "@serialport/parser-slip-encoder": "10.3.0",
+        "@serialport/parser-spacepacket": "10.3.0",
+        "@serialport/stream": "10.3.0",
+        "debug": "^4.3.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/serialport/donate"
+      }
+    },
+    "node_modules/serialport/node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/serialport/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/serve-static": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+      "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+      "dependencies": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.17.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.0.tgz",
+      "integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  },
+  "dependencies": {
+    "@serialport/binding-mock": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
+      "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
+      "requires": {
+        "@serialport/bindings-interface": "^1.2.1",
+        "debug": "^4.3.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "@serialport/bindings-cpp": {
+      "version": "10.7.0",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.7.0.tgz",
+      "integrity": "sha512-Xx1wA2UCG2loS32hxNvWJI4smCzGKhWqE85//fLRzHoGgE1lSLe3Nk7W40/ebrlGFHWRbQZmeaIF4chb2XLliA==",
+      "requires": {
+        "@serialport/bindings-interface": "1.2.1",
+        "@serialport/parser-readline": "^10.2.1",
+        "debug": "^4.3.2",
+        "node-addon-api": "^4.3.0",
+        "node-gyp-build": "^4.3.0"
+      },
+      "dependencies": {
+        "@serialport/bindings-interface": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.1.tgz",
+          "integrity": "sha512-63Dyqz2gtryRDDckFusOYqLYhR3Hq/M4sEdbF9i/VsvDb6T+tNVgoAKUZ+FMrXXKnCSu+hYbk+MTc0XQANszxw=="
+        },
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "@serialport/bindings-interface": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
+      "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA=="
+    },
+    "@serialport/parser-byte-length": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.3.0.tgz",
+      "integrity": "sha512-pJ/VoFemzKRRNDHLhFfPThwP40QrGaEnm9TtwL7o2GihEPwzBg3T0bN13ew5TpbbUYZdMpUtpm3CGfl6av9rUQ=="
+    },
+    "@serialport/parser-cctalk": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.3.0.tgz",
+      "integrity": "sha512-8ujmk8EvVbDPrNF4mM33bWvUYJOZ0wXbY3WCRazHRWvyCdL0VO0DQvW81ZqgoTpiDQZm5r8wQu9rmuemahF6vQ=="
+    },
+    "@serialport/parser-delimiter": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.3.0.tgz",
+      "integrity": "sha512-9E4Vj6s0UbbcCCTclwegHGPYjJhdm9qLCS0lowXQDEQC5naZnbsELemMHs93nD9jHPcyx1B4oXkMnVZLxX5TYw=="
+    },
+    "@serialport/parser-inter-byte-timeout": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.3.0.tgz",
+      "integrity": "sha512-wKP0QK85NHgvT6BBB1qBfKBBU4pf8kespNXAZBUYmFT+P4n8r8IZE2mqigCD+AiZcfWNQoAizwOsT/Jx/qeVig=="
+    },
+    "@serialport/parser-packet-length": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.3.0.tgz",
+      "integrity": "sha512-bj0cWzt8YSQj/E5fRQVYdi4TsfTlZQrXlXrUwjyTsCONv8IPOHzsz+yY0fw5SEMiJtaLyqvPkCHLsttOd/zFsg=="
+    },
+    "@serialport/parser-readline": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.3.0.tgz",
+      "integrity": "sha512-ki3ATZ3/RAqnqGROBKE7k+OeZ0DZXZ53GTca4q71OU5RazbbNhTOBQLKLXD3v9QZXCMJdg4hGW/2Y0DuMUqMQg==",
+      "requires": {
+        "@serialport/parser-delimiter": "10.3.0"
+      }
+    },
+    "@serialport/parser-ready": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.3.0.tgz",
+      "integrity": "sha512-1owywJ4p592dJyVrEJZPIh6pUZ3/y/LN6kGTDH2wxdewRUITo/sGvDy0er5i2+dJD3yuowiAz0dOHSdz8tevJA=="
+    },
+    "@serialport/parser-regex": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.3.0.tgz",
+      "integrity": "sha512-tIogTs7CvTH+UUFnsvE7i33MSISyTPTGPWlglWYH2/5coipXY503jlaYS1YGe818wWNcSx6YAjMZRdhTWwM39w=="
+    },
+    "@serialport/parser-slip-encoder": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.3.0.tgz",
+      "integrity": "sha512-JI0ILF5sylWn8f0MuMzHFBix/iMUTa79/Z95KaPZYnVaEdA7h7hh/o21Jmon/26P3RJwL1SNJCjZ81zfan+LtQ=="
+    },
+    "@serialport/parser-spacepacket": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.3.0.tgz",
+      "integrity": "sha512-PDF73ClEPsClD1FEJZHNuBevDKsJCkqy/XD5+S5eA6+tY5D4HLrVgSWsg+3qqB6+dlpwf2CzHe+uO8D3teuKHA=="
+    },
+    "@serialport/stream": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.3.0.tgz",
+      "integrity": "sha512-7sooi5fHogYNVEJwxVdg872xO6TuMgQd2E9iRmv+o8pk/1dbBnPkmH6Ka3st1mVE+0KnIJqVlgei+ncSsqXIGw==",
+      "requires": {
+        "@serialport/bindings-interface": "1.2.1",
+        "debug": "^4.3.2"
+      },
+      "dependencies": {
+        "@serialport/bindings-interface": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.1.tgz",
+          "integrity": "sha512-63Dyqz2gtryRDDckFusOYqLYhR3Hq/M4sEdbF9i/VsvDb6T+tNVgoAKUZ+FMrXXKnCSu+hYbk+MTc0XQANszxw=="
+        },
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      }
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "body-parser": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
+      "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
+      "requires": {
+        "bytes": "3.1.1",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.9.6",
+        "raw-body": "2.4.2",
+        "type-is": "~1.6.18"
+      }
+    },
+    "bytes": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
+      "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg=="
+    },
+    "content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "requires": {
+        "safe-buffer": "5.2.1"
+      }
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+    },
+    "cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "esm": {
+      "version": "3.2.25",
+      "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+      "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+    },
+    "express": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
+      "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
+      "requires": {
+        "accepts": "~1.3.7",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.19.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.1.2",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.9.6",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.17.2",
+        "serve-static": "1.14.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+    },
+    "http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+    },
+    "mime-db": {
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+    },
+    "mime-types": {
+      "version": "2.1.34",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+      "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+      "requires": {
+        "mime-db": "1.51.0"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+    },
+    "node-addon-api": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+      "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
+    },
+    "node-gyp-build": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",
+      "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ=="
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "requires": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      }
+    },
+    "qs": {
+      "version": "6.9.6",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
+      "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
+    },
+    "range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+    },
+    "raw-body": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
+      "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
+      "requires": {
+        "bytes": "3.1.1",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "send": {
+      "version": "0.17.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+      "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.8.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
+      },
+      "dependencies": {
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "serialport": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/serialport/-/serialport-10.4.0.tgz",
+      "integrity": "sha512-PszPM5SnFMgSXom60PkKS2A9nMlNbHkuoyRBlzdSWw9rmgOn258+V0dYbWMrETJMM+TJV32vqBzjg5MmmUMwMw==",
+      "requires": {
+        "@serialport/binding-mock": "10.2.2",
+        "@serialport/bindings-cpp": "10.7.0",
+        "@serialport/parser-byte-length": "10.3.0",
+        "@serialport/parser-cctalk": "10.3.0",
+        "@serialport/parser-delimiter": "10.3.0",
+        "@serialport/parser-inter-byte-timeout": "10.3.0",
+        "@serialport/parser-packet-length": "10.3.0",
+        "@serialport/parser-readline": "10.3.0",
+        "@serialport/parser-ready": "10.3.0",
+        "@serialport/parser-regex": "10.3.0",
+        "@serialport/parser-slip-encoder": "10.3.0",
+        "@serialport/parser-spacepacket": "10.3.0",
+        "@serialport/stream": "10.3.0",
+        "debug": "^4.3.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+      "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.17.2"
+      }
+    },
+    "setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+    },
+    "toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+    },
+    "ws": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.0.tgz",
+      "integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==",
+      "requires": {}
+    }
+  }
+}
diff --git a/system/javascript/package.json b/system/javascript/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c05c7e067947d8b8eb42cdc87057896c34f1d1a5
--- /dev/null
+++ b/system/javascript/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "minimal-controller",
+  "version": "1.0.0",
+  "description": "",
+  "main": "controller.js",
+  "module": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "esm": "^3.2.25",
+    "express": "^4.17.2",
+    "serialport": "^10.4.0",
+    "ws": "^8.4.0"
+  }
+}