diff --git a/README.md b/README.md index bc67903..2029f27 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Requires **Blender 5.2 LTS** or newer (files were written with 5.2.3). | Folder | What is in it | |---|---| -| [`instruments/`](instruments/) | One `.blend` per ARM instrument (AERI, KAZR, DL, MPL, MWR3C, MET tower, ECOR, SEBS, SIRS, RWP, …) plus the C-SAPR2 radar, the KAWG AWOS and the radiosonde balloon launch (balloon, sonde, line and launch crew, animated) | +| [`instruments/`](instruments/) | One `.blend` per ARM instrument (AERI, KAZR, DL, MPL, MWR3C, MET tower, ECOR, SEBS, SIRS, RWP, …) plus the C-SAPR2 radar, the KAWG AWOS, the radiosonde balloon launch (balloon, sonde, line and launch crew, animated), and the DERECHOS additions: MRR micro rain radar, stand-alone METWXT, GNDRAD upwelling radiometers with ground-viewing IRT, STAMP and SoilVUE10 soil profiles, and a PhenoCam mast | | [`containers/`](containers/) | AMF2 instrument containers, AOS platform, office shelter, and the SACR container (M1 and S2 versions) | | [`buildings/`](buildings/) | Farmstead (M1), MIRF buildings and center pivots (S1), KAWG hangars and terminal (S2), Louisa power station, flagpole, site signs | | [`vehicles/`](vehicles/) | Farm vehicles and implements, and the ARM Challenger 850 research aircraft | diff --git a/instruments/GNDRAD/GNDRAD.blend b/instruments/GNDRAD/GNDRAD.blend new file mode 100644 index 0000000..6a46013 Binary files /dev/null and b/instruments/GNDRAD/GNDRAD.blend differ diff --git a/instruments/METWXT/METWXT.blend b/instruments/METWXT/METWXT.blend new file mode 100644 index 0000000..466618c Binary files /dev/null and b/instruments/METWXT/METWXT.blend differ diff --git a/instruments/MRR/MRR.blend b/instruments/MRR/MRR.blend new file mode 100644 index 0000000..4985f7a Binary files /dev/null and b/instruments/MRR/MRR.blend differ diff --git a/instruments/PHENOCAM/PHENOCAM.blend b/instruments/PHENOCAM/PHENOCAM.blend new file mode 100644 index 0000000..bb42c11 Binary files /dev/null and b/instruments/PHENOCAM/PHENOCAM.blend differ diff --git a/instruments/README.md b/instruments/README.md index ab96d8c..53f1fa2 100644 --- a/instruments/README.md +++ b/instruments/README.md @@ -4,6 +4,8 @@ One file per instrument; link the `INST_*` (or `CSAPR2`) collection. Root at the `INST_BALLOON` is the radiosonde launch at M1: balloon, sonde, the line (a stretch-to constraint from sonde to balloon) and the two-person launch crew (`BALLOON_Crew`). It carries the release animation over frames 1–480; the *_Hold/*_Release crew poses swap visibility at release. +`INST_GNDRAD`, `INST_METWXT`, `INST_MRR`, `INST_PHENOCAM`, `INST_SOILVUE10` and `INST_STAMP` are procedural (rebuild with `blender -b --factory-startup --python tools/build_inst_.py`; helpers in `tools/instlib.py`). Each root carries `full_name`, `sites`, `sources` and `notes` custom properties. Parts below grade (probes, footings, driven stakes, ground rods) sit in a `_BelowGrade` child collection that is hidden in viewport and render; un-hide it for cut-away views. Their `bbox_*` describe what stands above ground and `below_grade_m` gives the depth. + | Preview | Collection | File | |---|---|---| | | `INST_AERI` | [`AERI/AERI.blend`](AERI/AERI.blend) (0.2 MB) | @@ -17,18 +19,24 @@ One file per instrument; link the `INST_*` (or `CSAPR2`) collection. Root at the | | `INST_DL` | [`DL/DL.blend`](DL/DL.blend) (0.2 MB) | | | `INST_ECOR` | [`ECOR/ECOR.blend`](ECOR/ECOR.blend) (0.3 MB) | | | `INST_ECOR_4m` | [`ECOR/ECOR.blend`](ECOR/ECOR.blend) (0.3 MB) | +| | `INST_GNDRAD` | [`GNDRAD/GNDRAD.blend`](GNDRAD/GNDRAD.blend) (0.2 MB) | | | `INST_IRT` | [`IRT/IRT.blend`](IRT/IRT.blend) (0.1 MB) | | | `INST_KAZR` | [`KAZR/KAZR.blend`](KAZR/KAZR.blend) (0.2 MB) | | | `INST_LDIS` | [`LDIS/LDIS.blend`](LDIS/LDIS.blend) (0.1 MB) | | | `INST_MAWS` | [`MAWS/MAWS.blend`](MAWS/MAWS.blend) (0.2 MB) | | | `INST_MET` | [`MET/MET.blend`](MET/MET.blend) (0.3 MB) | +| | `INST_METWXT` | [`METWXT/METWXT.blend`](METWXT/METWXT.blend) (0.2 MB) | | | `INST_MFR` | [`MFR/MFR.blend`](MFR/MFR.blend) (0.1 MB) | | | `INST_MFRSR` | [`MFRSR/MFRSR.blend`](MFRSR/MFRSR.blend) (0.1 MB) | | | `INST_MPL` | [`MPL/MPL.blend`](MPL/MPL.blend) (0.1 MB) | +| | `INST_MRR` | [`MRR/MRR.blend`](MRR/MRR.blend) (0.2 MB) | | | `INST_MWR3C` | [`MWR3C/MWR3C.blend`](MWR3C/MWR3C.blend) (0.2 MB) | +| | `INST_PHENOCAM` | [`PHENOCAM/PHENOCAM.blend`](PHENOCAM/PHENOCAM.blend) (0.1 MB) | | | `INST_RWP` | [`RWP/RWP.blend`](RWP/RWP.blend) (0.3 MB) | | | `INST_SEBS` | [`SEBS/SEBS.blend`](SEBS/SEBS.blend) (0.2 MB) | | | `INST_SIRS` | [`SIRS/SIRS.blend`](SIRS/SIRS.blend) (0.3 MB) | +| | `INST_SOILVUE10` | [`SOILVUE10/SOILVUE10.blend`](SOILVUE10/SOILVUE10.blend) (0.2 MB) | | | `INST_SONDE` | [`SONDE/SONDE.blend`](SONDE/SONDE.blend) (0.2 MB) | +| | `INST_STAMP` | [`STAMP/STAMP.blend`](STAMP/STAMP.blend) (0.2 MB) | | | `INST_VDIS` | [`VDIS/VDIS.blend`](VDIS/VDIS.blend) (0.1 MB) | | | `INST_WB` | [`WB/WB.blend`](WB/WB.blend) (0.2 MB) | diff --git a/instruments/SOILVUE10/SOILVUE10.blend b/instruments/SOILVUE10/SOILVUE10.blend new file mode 100644 index 0000000..bac2ffb Binary files /dev/null and b/instruments/SOILVUE10/SOILVUE10.blend differ diff --git a/instruments/STAMP/STAMP.blend b/instruments/STAMP/STAMP.blend new file mode 100644 index 0000000..11a3aeb Binary files /dev/null and b/instruments/STAMP/STAMP.blend differ diff --git a/instruments/preview/GNDRAD.jpg b/instruments/preview/GNDRAD.jpg new file mode 100644 index 0000000..4e4e23d Binary files /dev/null and b/instruments/preview/GNDRAD.jpg differ diff --git a/instruments/preview/METWXT.jpg b/instruments/preview/METWXT.jpg new file mode 100644 index 0000000..b51e50f Binary files /dev/null and b/instruments/preview/METWXT.jpg differ diff --git a/instruments/preview/MRR.jpg b/instruments/preview/MRR.jpg new file mode 100644 index 0000000..be1c9a3 Binary files /dev/null and b/instruments/preview/MRR.jpg differ diff --git a/instruments/preview/PHENOCAM.jpg b/instruments/preview/PHENOCAM.jpg new file mode 100644 index 0000000..69dc870 Binary files /dev/null and b/instruments/preview/PHENOCAM.jpg differ diff --git a/instruments/preview/SOILVUE10.jpg b/instruments/preview/SOILVUE10.jpg new file mode 100644 index 0000000..9d5be23 Binary files /dev/null and b/instruments/preview/SOILVUE10.jpg differ diff --git a/instruments/preview/STAMP.jpg b/instruments/preview/STAMP.jpg new file mode 100644 index 0000000..7549d9e Binary files /dev/null and b/instruments/preview/STAMP.jpg differ diff --git a/tools/build_inst_gndrad.py b/tools/build_inst_gndrad.py new file mode 100644 index 0000000..6d024d4 --- /dev/null +++ b/tools/build_inst_gndrad.py @@ -0,0 +1,134 @@ +"""GNDRAD upwelling radiometer stand (+ downward IRT, the GNDIRT) -> instruments/GNDRAD/GNDRAD.blend +(collection INST_GNDRAD). blender -b --factory-startup --python tools/build_inst_gndrad.py + +Per the GNDRAD handbook (ARM TR-027, T. Stoffel): upwelling shortwave by an inverted +pyranometer (Eppley PSP, 2pi sr, 0.3-3 um) and upwelling longwave by an inverted +pyrgeometer (Eppley PIR, 4-50 um), both mounted inverted WITHOUT ventilation 10 m above +ground looking at the surface. DERECHOS Table 1 also requests a ground-viewing IRT +(GNDIRT), which ARM mounts on the same boom. + +Model: guyed 10 m tilt-down aluminium mast on a concrete pad, 2 m boom to the south (-Y) +so the sensors view undisturbed ground away from the mast, crossarm at the boom tip with +PSP (west), IRT (centre, nadir) and PIR (east). Radiometer bodies are generic PSP/PIR-sized +(the field units may be Kipp & Zonen CMP22/CGR4); boom length and guy layout are generic. +""" +import bpy, math, os, sys +from mathutils import Vector, Matrix +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'GNDRAD' +coll, root = L.start(NAME) +HB = 10.0 # boom height (handbook: 10 m AGL) +BOOM = 2.0 # boom length towards -Y +TIP = Vector((0, -BOOM, HB)) +PLATE_C = -0.030 - 0.051 # radiometer case centre: shield top meets the plate underside + + +def build_mast(mb): + mb.box((0, 0, 0.025), (0.9, 0.9, 0.05), 'concrete') + mb.box((0, 0, 0.06), (0.36, 0.36, 0.02), 'galvanized') + for sx in (-1, 1): # tilt-down hinge cheeks + mb.box((sx * 0.07, 0, 0.17), (0.012, 0.16, 0.20), 'galvanized') + mb.cyl((-0.09, 0, 0.22), (0.09, 0, 0.22), 0.015, 'stainless', seg=12) + for x in (-0.13, 0.13): + for y in (-0.13, 0.13): + mb.cyl((x, y, 0.07), (x, y, 0.11), 0.012, 'stainless', seg=8) + secs = [(0.07, 4.0, 0.060), (4.0, 7.5, 0.048), (7.5, HB + 0.35, 0.038)] + for z0, z1, r in secs: + mb.cyl((0, 0, z0), (0, 0, z1), r, 'aluminum', seg=24) + mb.cyl((0, 0, z1 - 0.05), (0, 0, z1 + 0.02), r + 0.008, 'aluminum', seg=24) # splice collar + for zg in (4.5, 9.0): # guy rings + mb.cyl((0, 0, zg - 0.03), (0, 0, zg + 0.03), 0.065, 'galvanized', seg=20) + # air terminal + mb.cyl((0, 0, HB + 0.35), (0, 0, HB + 0.95), 0.008, 'copper', r1=0.003, seg=8) + # boom, brace and crossarm + mb.beam((0, -0.05, HB), TIP, 0.05, 0.05, 'aluminum') + mb.box((0, -0.06, HB), (0.14, 0.04, 0.16), 'galvanized') # boom clamp + mb.cyl((0, -0.04, HB - 0.75), (0, -1.2, HB - 0.03), 0.014, 'aluminum', seg=10) + mb.box((0, -0.05, HB - 0.75), (0.12, 0.04, 0.08), 'galvanized') + mb.beam(TIP + Vector((-0.40, 0, 0)), TIP + Vector((0.40, 0, 0)), 0.04, 0.04, 'aluminum') + mb.box(TIP, (0.10, 0.10, 0.07), 'galvanized') # crossarm clamp + for x in (-0.28, 0.28): # inverted mounting plates + mb.box(TIP + Vector((x, 0, -0.025)), (0.15, 0.15, 0.010), 'aluminum') # top touches the arm + # junction box at the boom root, logger enclosure at the base (door -Y) + mb.box((0.10, -0.10, HB - 0.35), (0.16, 0.10, 0.20), 'grey_plastic') + mb.box((0, -0.17, 1.45), (0.40, 0.20, 0.50), 'fiberglass_white') + mb.box((0, -0.273, 1.45), (0.38, 0.008, 0.48), 'fiberglass_white') + mb.box((0, -0.278, 1.6), (0.12, 0.002, 0.05), 'label') + for z in (1.25, 1.65): + mb.box((0, -0.06, z), (0.36, 0.04, 0.04), 'galvanized') + + +def radiometer(mb, c, kind): + """Inverted PSP ('sw') or PIR ('lw') body centred at c, domes pointing down (-Z).""" + c = Vector(c) + body_top = c + Vector((0, 0, 0.045)) + mb.cyl(body_top, body_top + Vector((0, 0, 0.006)), 0.085, 'kipp_white', seg=40) # sun/guard shield + mb.cyl(c, body_top, 0.064, 'kipp_white', seg=40) # case + mb.cyl(c - Vector((0, 0, 0.012)), c, 0.058, 'black_anod', seg=40) # base ring + for k in range(3): # levelling screws + a = math.radians(90 + 120 * k) + p = c + Vector((0.075 * math.cos(a), 0.075 * math.sin(a), 0.045)) + mb.cyl(p, p + Vector((0, 0, 0.02)), 0.006, 'stainless', seg=8) + mb.cyl(c + Vector((0.06, 0, 0.02)), c + Vector((0.10, 0, 0.02)), 0.010, 'black_plastic', seg=10) # connector + d0 = c - Vector((0, 0, 0.012)) + if kind == 'sw': # double glass domes over a black detector + mb.cyl(d0, d0 - Vector((0, 0, 0.002)), 0.022, 'black_body', seg=24) + mb.lathe([(0.032 * math.cos(t), -0.032 * math.sin(t)) for t in [i * math.pi / 2 / 10 for i in range(11)]], + 'dome', M=Matrix.Translation(d0), seg=40) + mb.lathe([(0.020 * math.cos(t), -0.020 * math.sin(t)) for t in [i * math.pi / 2 / 8 for i in range(9)]], + 'dome', M=Matrix.Translation(d0), seg=32) + else: # flat-ish silicon window (pyrgeometer) + mb.cyl(d0, d0 - Vector((0, 0, 0.010)), 0.030, 'black_anod', r1=0.026, seg=32) + mb.lathe([(0.026, 0.0), (0.024, -0.004), (0.016, -0.008), (0.0, -0.009)], 'lens_dark', + M=Matrix.Translation(d0 - Vector((0, 0, 0.010))), seg=32) + + +def build_sensors(mb): + radiometer(mb, TIP + Vector((-0.28, 0, PLATE_C)), 'sw') + radiometer(mb, TIP + Vector((0.28, 0, PLATE_C)), 'lw') + # nadir IRT (Heitronics KT15-class head) hanging from the crossarm clamp + mb.box(TIP + Vector((0, 0, -0.0525)), (0.06, 0.06, 0.065), 'aluminum') + mb.cyl(TIP + Vector((0, 0, -0.08)), TIP + Vector((0, 0, -0.28)), 0.033, 'stainless', seg=28) + mb.cyl(TIP + Vector((0, 0, -0.28)), TIP + Vector((0, 0, -0.34)), 0.036, 'black_anod', seg=28) + mb.cyl(TIP + Vector((0, 0, -0.340)), TIP + Vector((0, 0, -0.343)), 0.018, 'lens_dark', seg=24) + + +L.part('gndrad_mast', coll, root, build_mast) +L.part('gndrad_sensors', coll, root, build_sensors) +L.empty('gndrad_psp', coll, root, tuple(TIP + Vector((-0.28, 0, PLATE_C))), 0.1) +L.empty('gndrad_pir', coll, root, tuple(TIP + Vector((0.28, 0, PLATE_C))), 0.1) +L.empty('gndirt', coll, root, tuple(TIP + Vector((0, 0, -0.34))), 0.1) + +# guys: three directions avoiding the boom (boom -Y = 270 deg) +for k, az in enumerate((90, 210, 330)): + a = math.radians(az) + anchor = Vector((5.0 * math.cos(a), 5.0 * math.sin(a), 0.0)) + L.part(f'gndrad_anchor_{k}', coll, root, lambda mb, p=anchor: ( + mb.cyl(p + Vector((0, 0, 0.0)), p + Vector((0, 0, 0.12)), 0.02, 'galvanized', seg=10), + mb.box(p + Vector((0, 0, 0.0)), (0.2, 0.2, 0.02), 'concrete'))) + for zg in (4.5, 9.0): + top = Vector((0.065 * math.cos(a), 0.065 * math.sin(a), zg)) + L.wire(f'gndrad_guy_{k}_{int(zg)}', top, anchor + Vector((0, 0, 0.12)), 0.003, coll, root) + +L.cable('gndrad_cable_up', [(0.04, -0.17, 1.70), (0.045, -0.05, 1.9), (0.045, -0.045, 5.0), + (0.045, -0.045, HB - 0.5), (0.10, -0.10, HB - 0.45)], 0.008, coll, root) +for x, nm in ((-0.28, 'psp'), (0.0, 'irt'), (0.28, 'pir')): + L.cable(f'gndrad_cable_{nm}', [(0.10, -0.10, HB - 0.25), (0.06, -0.4, HB - 0.06), (x * 0.5, -1.4, HB - 0.03), + (x, -BOOM + 0.12, HB - 0.04), (x + (0.1 if x >= 0 else 0.1), -BOOM, HB - 0.08)], + 0.004, coll, root) +L.cable('gndrad_cable_power', [(-0.08, -0.17, 1.20), (-0.08, -0.22, 0.6), (-0.10, -0.30, 0.06), + (-0.4, -0.7, 0.0), (-0.8, -1.1, 0.0)], 0.009, coll, root) + +L.finish(NAME, coll, root, + 'Ground Radiometers on Stand for Upwelling Radiation (GNDRAD) with ground-viewing IRT', + ['M1', 'S1'], + ['ARM GNDRAD handbook ARM TR-027 (T. Stoffel, 2004): inverted unventilated PSP and PIR at 10 m AGL', + 'DERECHOS white paper Table 1 (SIRS upwelling components; GNDIRT / IRT row)'], + f'Guyed 10 m tilt-down aluminium mast on a concrete pad; {BOOM:.0f} m boom to -Y at {HB:.0f} m; ' + 'crossarm at the tip with inverted PSP-type pyranometer (empty gndrad_psp, -X), nadir IRT ' + '(gndirt, centre) and inverted PIR-type pyrgeometer (gndrad_pir, +X). Guys at 4.5 and 9 m ' + 'to anchors 5 m out at az 90/210/330 deg (clear of the boom). Logger enclosure at 1.2-1.7 m. ' + 'Sensor bodies and stand layout are generic.') +print('ALL DONE') diff --git a/tools/build_inst_metwxt.py b/tools/build_inst_metwxt.py new file mode 100644 index 0000000..b02bc86 --- /dev/null +++ b/tools/build_inst_metwxt.py @@ -0,0 +1,106 @@ +"""Stand-alone Vaisala WXT530-series weather transmitter -> instruments/METWXT/METWXT.blend +(collection INST_METWXT). blender -b --factory-startup --python tools/build_inst_metwxt.py + +The configuration used on ARM redeployable profiling modules (R1) and the ANL guest kit +at NIU (DERECHOS Tables 1 and 3): a WXT530-series head on a ~2 m, 60 mm galvanised pole +with a folding tripod base, a fibreglass logger/comms enclosure and a ground rod. +The WXT head itself is a copy of the wxt530 sub-assembly of instruments/MET/MET.blend +(read only), so both assets show the same sensor. +""" +import bpy, math, os, sys +from mathutils import Vector, Matrix +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'METWXT' +coll, root = L.start(NAME) +H = 2.00 # top of the pole; WXT mount sits on it + +# ---- WXT head copied from the MET tower asset +src = os.path.join(L.REPO, 'instruments/MET/MET.blend') +with bpy.data.libraries.load(src, link=False) as (s, d): + d.objects = [n for n in s.objects if n.startswith('wxt530')] +objs = {o.name: o for o in d.objects} +old = objs.pop('wxt530') +head = L.empty('wxt', coll, root, (0, 0, 0), 0.15) +for o in objs.values(): + coll.objects.link(o) + o.parent = head # keeps local transform relative to the head +bpy.data.objects.remove(old) +# find the bottom of the head in its own frame and sit it on the pole top +zmin = min((o.matrix_parent_inverse @ o.matrix_basis @ Vector(c)).z for o in objs.values() for c in o.bound_box) +head.location = (0, 0, H - zmin) +for o in objs.values(): + o.name = o.name.replace('wxt530', 'wxt') + + +def build_mast(mb): + mb.cyl((0, 0, 0.0), (0, 0, H), 0.0302, 'galvanized', seg=24) + mb.cyl((0, 0, H - 0.004), (0, 0, H), 0.031, 'black_plastic', seg=24) + mb.cyl((0, 0, 0.0), (0, 0, 0.012), 0.08, 'galvanized', seg=24) + # folding tripod: collar at 0.9 m, three legs to foot plates, ground stakes + mb.cyl((0, 0, 0.86), (0, 0, 0.94), 0.042, 'galvanized', seg=20) + for k in range(3): + a = math.radians(90 + 120 * k) + foot = Vector((0.78 * math.cos(a), 0.78 * math.sin(a), 0.02)) + top = Vector((0.045 * math.cos(a), 0.045 * math.sin(a), 0.90)) + mb.cyl(top, foot, 0.016, 'galvanized', seg=10) + mb.box(foot + Vector((0, 0, -0.012)), (0.12, 0.12, 0.008), 'galvanized') + mb.cyl(foot + Vector((0.04 * math.cos(a), 0.04 * math.sin(a), 0.03)), + foot + Vector((0.04 * math.cos(a), 0.04 * math.sin(a), -0.012)), 0.008, 'galvanized', seg=8) + mid = top.lerp(foot, 0.55) + mb.cyl(mid, Vector((0, 0, 0.35)), 0.009, 'galvanized', seg=8) # leg brace to the pole + mb.cyl((0, 0, 0.33), (0, 0, 0.37), 0.04, 'galvanized', seg=20) + + +def build_enclosure(mb): + # 14x12x6-inch fibreglass enclosure on unistrut, door facing south (-Y) + for z in (0.95, 1.40): + mb.box((0, -0.045, z), (0.30, 0.04, 0.04), 'galvanized') + mb.box((0, -0.03, z), (0.08, 0.03, 0.07), 'stainless') + mb.box((0, -0.16, 1.175), (0.31, 0.16, 0.36), 'fiberglass_white') + mb.box((0, -0.243, 1.175), (0.29, 0.008, 0.34), 'fiberglass_white') + for x in (-0.14, 0.14): + for z in (1.03, 1.32): + mb.cyl((x, -0.247, z), (x, -0.257, z), 0.012, 'stainless', seg=10) + mb.box((0, -0.248, 1.29), (0.10, 0.002, 0.04), 'label') + for x in (-0.08, 0.0, 0.08): + mb.cyl((x, -0.16, 0.995), (x, -0.16, 0.97), 0.011, 'grey_plastic', seg=10) + + +def build_ground(mb): + mb.cyl((0.35, -0.25, 0.0), (0.35, -0.25, 0.10), 0.008, 'copper', seg=10) + mb.box((0.35, -0.25, 0.08), (0.03, 0.03, 0.03), 'brass') + + +def build_buried(mb): + mb.cyl((0.35, -0.25, -0.40), (0.35, -0.25, 0.0), 0.008, 'copper', seg=10) # ground rod + for k in range(3): # tripod stakes + a = math.radians(90 + 120 * k) + f = Vector((0.82 * math.cos(a), 0.82 * math.sin(a), 0.0)) + mb.cyl(f, f + Vector((0, 0, -0.30)), 0.008, 'galvanized', seg=8) + + +L.part('metwxt_mast', coll, root, build_mast) +L.part('metwxt_enclosure', coll, root, build_enclosure) +L.part('metwxt_ground_rod', coll, root, build_ground) +L.part('metwxt_buried', L.below(coll), root, build_buried) +L.cable('metwxt_cable_wxt', [(0.0, -0.040, H - 0.05), (0.0, -0.040, 1.7), (0.0, -0.040, 1.45), + (0.08, -0.10, 1.0), (0.08, -0.16, 0.97)], 0.004, coll, root) +L.cable('metwxt_cable_power', [(-0.08, -0.16, 0.97), (-0.08, -0.20, 0.6), (-0.06, -0.12, 0.2), + (-0.10, -0.15, 0.01), (-0.45, -0.55, 0.0), (-0.7, -0.85, 0.0)], 0.007, coll, root) +L.cable('metwxt_ground_strap', [(0.0, -0.16, 0.97), (0.05, -0.28, 0.55), (0.25, -0.30, 0.15), (0.35, -0.25, 0.09)], + 0.003, coll, root, m='copper') + +L.finish(NAME, coll, root, + 'Vaisala WXT530-series weather transmitter on stand-alone pole (METWXT)', + ['R1', 'NIU'], + ['ARM MET handbook DOE/SC-ARM-TR-086 (metwxt class)', + 'Vaisala WXT530 series user guide M211840EN (WXT536 0.7 kg, polycarbonate shield, AISI 316 rain plate)', + 'DERECHOS white paper Table 1 (MET / METWXT at M1, S1, R1) and Table 3 (Vaisala WXT530 at NIU)', + 'WXT head geometry copied from instruments/MET/MET.blend (wxt530 sub-assembly)'], + f'WXT head (empty "wxt") on a 60 mm galvanised pole, sensor base at {H:.2f} m (head top ~{H + 0.24:.2f} m); ' + 'folding tripod with stakes, fibreglass logger/comms enclosure at 1.0-1.35 m facing -Y, ' + 'ground rod (stakes and rod below grade in METWXT_BelowGrade). North mark of the WXT is +Y. Generic stand - match to the R1 module ' + 'or ADM installation when its layout is fixed.') +print('ALL DONE') diff --git a/tools/build_inst_mrr.py b/tools/build_inst_mrr.py new file mode 100644 index 0000000..4ff6127 --- /dev/null +++ b/tools/build_inst_mrr.py @@ -0,0 +1,147 @@ +"""METEK MRR-PRO micro rain radar -> instruments/MRR/MRR.blend (collection INST_MRR). +blender -b --factory-startup --python tools/build_inst_mrr.py + +Geometry: 24 GHz FMCW, vertically pointing offset-paraboloid antenna, 600 mm aperture +(METEK MRR-PRO datasheet 2024-07-05: offset antenna 600 mm, 40.1 dBi, 1.5 deg beam, ~20 kg). +The reflector is a true offset section of z = (x^2+y^2)/(4f) with the focus on the +paraboloid axis, so the beam leaves vertically; the transceiver/feed housing sits at the +focus on the -X side and looks down-and-across into the dish (classic MRR silhouette). +Mast, base frame and junction box are generic (no ARM installation drawing available). +""" +import bpy, math, os, sys +from mathutils import Vector +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'MRR' +coll, root = L.start(NAME) + +F = 0.40 # focal length (m) - f/D ~ 0.67 typical of offset dishes +R = 0.30 # aperture radius (600 mm) +X0 = 0.42 # offset of the aperture centre from the paraboloid axis +ZV = 1.02 # height of the (virtual) paraboloid vertex +MAST_X = X0 # mast under the aperture centre + + +def dish_z(x, y): + return ZV + (x * x + y * y) / (4 * F) + + +def dish_pt(rr, th, off=0.0): + rr = max(rr, 1e-3) # avoid a collapsed centre ring + xa = X0 + rr * math.cos(th) # in paraboloid coordinates + y = rr * math.sin(th) + z = dish_z(xa, y) + # surface normal of z = r^2/4f points (-x/2f, -y/2f, 1) + n = Vector((-xa / (2 * F), -y / (2 * F), 1)).normalized() + p = Vector((xa - MAST_X, y, z)) + n * off + return tuple(p) + + +# local frame: mast at origin; paraboloid axis at x = -MAST_X +AX = -MAST_X +FOCUS = Vector((AX, 0, ZV + F)) +DISH_C = Vector(dish_pt(0, 0)) + + +def build_base(mb): + # square galvanised ground frame with levelling feet and a 60 mm mast + for sx in (-1, 1): + mb.box((0, sx * 0.30, 0.03), (0.70, 0.05, 0.05), 'galvanized') + mb.box((sx * 0.30, 0, 0.03), (0.05, 0.70, 0.05), 'galvanized') + for sy in (-1, 1): + mb.cyl((sx * 0.33, sy * 0.33, 0.0), (sx * 0.33, sy * 0.33, 0.06), 0.035, 'galvanized', seg=16) + mb.cyl((sx * 0.33, sy * 0.33, 0.0), (sx * 0.33, sy * 0.33, 0.012), 0.06, 'black_plastic', seg=16) + # diagonal braces up to the mast + mb.cyl((sx * 0.28, sy * 0.28, 0.05), (sx * 0.03, sy * 0.03, 0.45), 0.012, 'galvanized', seg=8) + mb.box((0, 0, 0.065), (0.16, 0.16, 0.012), 'galvanized') + mb.cyl((0, 0, 0.07), (0, 0, ZV - 0.06), 0.0302, 'galvanized', seg=24) + mb.cyl((0, 0, ZV - 0.06), (0, 0, ZV - 0.055), 0.034, 'galvanized', seg=24) + + +def build_reflector(mb): + nr, nt = 10, 64 + front = [[dish_pt(R * i / nr, 2 * math.pi * j / nt) for j in range(nt)] for i in range(nr + 1)] + back = [[dish_pt(R * i / nr, 2 * math.pi * j / nt, -0.012) for j in range(nt)] for i in range(nr + 1)] + mb.grid(front, 'radar_white', close_u=True) + mb.grid(list(reversed(back)), 'radar_white', close_u=True) + rim = [front[-1], [dish_pt(R + 0.012, 2 * math.pi * j / nt, -0.006) for j in range(nt)], back[-1]] + mb.grid(rim, 'radar_white', close_u=True) + + +def build_backframe(mb): + # yoke on the mast top, two arms to the reflector back, ring stiffener + top = Vector((0, 0, ZV - 0.055)) + mb.box(top + Vector((0, 0, 0.02)), (0.18, 0.12, 0.012), 'galvanized') + for th in (math.radians(140), math.radians(220), math.radians(0)): + p = Vector(dish_pt(0.62 * R, th, -0.02)) + mb.cyl(top + Vector((0, 0, 0.02)), p, 0.011, 'galvanized', seg=8) + mb.box(p, (0.06, 0.06, 0.008), 'galvanized') + ring = [dish_pt(0.62 * R, 2 * math.pi * j / 32, -0.025) for j in range(33)] + for a, b in zip(ring, ring[1:]): + mb.cyl(a, b, 0.008, 'aluminum', seg=6) + + +AXIS = (DISH_C - FOCUS).normalized() # feed boresight +RIM_X = X0 - R - MAST_X - 0.03 # just outside the low rim of the reflector + + +def build_feed(mb): + # transceiver housing behind the focus, conical horn at the focus + h0 = FOCUS - AXIS * 0.02 + mb.cyl(h0, FOCUS + AXIS * 0.06, 0.028, 'metek_grey', r1=0.055, seg=24) # horn + mb.cyl(FOCUS + AXIS * 0.06, FOCUS + AXIS * 0.062, 0.050, 'black_plastic', seg=24) # radome cap + mb.cyl(h0 - AXIS * 0.26, h0, 0.075, 'metek_grey', seg=28) # transceiver body + mb.cyl(h0 - AXIS * 0.275, h0 - AXIS * 0.26, 0.078, 'metek_grey', seg=28) + for k in range(8): # cooling fins + c = h0 - AXIS * (0.04 + 0.025 * k) + mb.cyl(c, c - AXIS * 0.006, 0.083, 'aluminum', seg=28) + mb.cyl(h0 - AXIS * 0.275, h0 - AXIS * 0.30, 0.022, 'black_plastic', seg=12) # connector + # feed arm: from a bracket below the reflector's low rim (outside the aperture) to a + # clamp on the housing; a strut ties the bracket back to the mast yoke + clamp = h0 - AXIS * 0.14 + side = Vector((0, 1, 0)) + brk = Vector((RIM_X, 0, ZV - 0.035)) + mb.box(brk, (0.05, 0.30, 0.03), 'galvanized') + mb.cyl(Vector((0, 0, ZV - 0.035)), brk, 0.014, 'galvanized', seg=10) + for s in (-1, 1): + mb.cyl(brk + side * s * 0.12, clamp + side * s * 0.07, 0.012, 'galvanized', seg=8) + mb.box(clamp + side * s * 0.075, (0.02, 0.02, 0.05), 'galvanized') + mb.cyl(clamp - side * 0.085, clamp + side * 0.085, 0.006, 'stainless', seg=8) + + +def build_box(mb): + # power / heater / network junction box on the mast + mb.box((0, -0.135, 0.55), (0.30, 0.17, 0.40), 'grey_plastic') + mb.box((0, -0.222, 0.55), (0.26, 0.006, 0.36), 'grey_plastic') + mb.box((0, -0.226, 0.66), (0.12, 0.002, 0.05), 'label') + for x in (-0.07, 0.0, 0.07): + mb.cyl((x, -0.135, 0.35), (x, -0.135, 0.325), 0.012, 'black_plastic', seg=10) + for z in (0.40, 0.70): + mb.box((0, -0.04, z), (0.12, 0.03, 0.03), 'stainless') + + +L.part('mrr_base', coll, root, build_base) +L.part('mrr_reflector', coll, root, build_reflector) +L.part('mrr_backframe', coll, root, build_backframe) +L.part('mrr_transceiver', coll, root, build_feed) +L.part('mrr_junction_box', coll, root, build_box) +L.empty('mrr_focus', coll, root, tuple(FOCUS), 0.1) +tail = FOCUS - AXIS * 0.31 +L.cable('mrr_cable_rf', [(0.0, -0.135, 0.75), (0.0, -0.10, 0.80), (0.0, -0.036, 0.86), + (0.0, -0.036, ZV - 0.07), (RIM_X * 0.5, -0.03, ZV - 0.06), + (RIM_X, -0.14, ZV - 0.02), tuple(Vector((RIM_X, -0.13, ZV)).lerp(tail, 0.5) + Vector((0, 0, -0.02))), + tuple(tail)], 0.008, coll, root) +L.cable('mrr_cable_power', [(-0.07, -0.135, 0.33), (-0.07, -0.16, 0.12), (-0.10, -0.22, 0.02), + (-0.25, -0.45, 0.0), (-0.40, -0.62, 0.0)], 0.009, coll, root) + +L.finish(NAME, coll, root, + 'METEK MRR-PRO Micro Rain Radar (24 GHz FMCW, vertically pointing)', + ['R1', 'NIU'], + ['METEK MRR-PRO datasheet 2024-07-05: offset antenna 600 mm, 40.1 dBi, 1.5 deg 3 dB beamwidth, ~20 kg', + 'DERECHOS white paper Table 1 (MRR, R1, priority 3) and Table 3 (METEK MRR at NIU)'], + 'Offset paraboloid (D 600 mm, f 400 mm, offset 420 mm) on a 60 mm galvanised mast with a ' + 'square levelling frame; transceiver/feed at the focus on the -X side, beam vertical over ' + 'the aperture centre (x = 0, top of dish ~1.2 m). Mast, frame and box are generic - adjust ' + 'to the as-deployed ARM/ANL mount when photos are available. Empty mrr_focus marks the feed.') +print('ALL DONE') diff --git a/tools/build_inst_phenocam.py b/tools/build_inst_phenocam.py new file mode 100644 index 0000000..c83f845 --- /dev/null +++ b/tools/build_inst_phenocam.py @@ -0,0 +1,95 @@ +"""PhenoCam-style land-surface camera mast -> instruments/PHENOCAM/PHENOCAM.blend +(collection INST_PHENOCAM). blender -b --factory-startup --python tools/build_inst_phenocam.py + +DERECHOS AI thrust 4 (computer vision for land-surface state: crop stage, canopy, +residue, snow, standing water) is built on site cameras/phenocams, which the white-paper +errata note Table 1 does not trace. This is the PhenoCam Network configuration: a StarDot +NetCam SC 5MP in its weatherproof housing with sun shield (the network's supported +camera, phenocam.nau.edu FAQ), pointed north (+Y) so the canopy is front-lit and the +image is dominated by the vegetation, tilted ~20 deg below the horizon, above a row-crop +canopy that reaches ~3 m (corn) - hence a 5 m guyed mast. +Housing dimensions and mast are generic. +""" +import bpy, math, os, sys +from mathutils import Vector, Matrix +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'PHENOCAM' +coll, root = L.start(NAME) +H = 5.0 # mast top +ARM = 0.45 # side arm towards +Y (view direction) +TILT = math.radians(20) # below horizon +CAM = Vector((0, ARM, H + 0.01)) # housing sits on the arm via the tilt mount +# camera frame: local +Y = boresight, tilted down by TILT about X +M = Matrix.Translation(CAM) @ Matrix.Rotation(-TILT, 4, 'X') +R3 = M.to_3x3() + + +def lp(x, y, z): + return M @ Vector((x, y, z)) + + +def build_mast(mb): + mb.box((0, 0, 0.03), (0.7, 0.7, 0.06), 'concrete') + mb.box((0, 0, 0.07), (0.25, 0.25, 0.015), 'galvanized') + mb.cyl((0, 0, 0.07), (0, 0, H), 0.0302, 'galvanized', seg=20) + mb.cyl((0, 0, H), (0, 0, H + 0.01), 0.033, 'black_plastic', seg=20) + mb.cyl((0, 0, 3.47), (0, 0, 3.53), 0.05, 'galvanized', seg=16) # guy ring + # side arm and tilt bracket + mb.box((0, 0.0, H - 0.08), (0.08, 0.08, 0.10), 'galvanized') + mb.beam(Vector((0, 0.03, H - 0.08)), Vector((0, ARM, H - 0.08)), 0.035, 0.035, 'aluminum') + mb.box(Vector((0, ARM - 0.02, H - 0.05)), (0.06, 0.06, 0.03), 'aluminum') # riser to the tilt mount + # network/PoE + surge enclosure at 1.3 m, door south (-Y) + mb.box((0, -0.12, 1.30), (0.26, 0.16, 0.31), 'fiberglass_white') + mb.box((0, -0.203, 1.30), (0.24, 0.006, 0.29), 'fiberglass_white') + mb.box((0, -0.207, 1.40), (0.09, 0.002, 0.035), 'label') + for z in (1.18, 1.42): + mb.box((0, -0.035, z), (0.18, 0.025, 0.03), 'galvanized') + mb.cyl((0.25, 0.15, 0.0), (0.25, 0.15, 0.10), 0.008, 'copper', seg=8) + + +def build_camera(mb): + # housing ~290 x 95 x 95 mm, sun shield on top overhanging the window + mb.box(lp(0, 0, 0), (0.095, 0.29, 0.095), 'stardot_grey', rot=R3) + mb.box(lp(0, 0.03, 0.058), (0.13, 0.36, 0.004), 'white_paint', rot=R3) + for x in (-0.05, 0.05): + mb.box(lp(x, 0.0, 0.053), (0.004, 0.20, 0.012), 'white_paint', rot=R3) + mb.box(lp(0, 0.1455, 0), (0.080, 0.002, 0.080), 'glass', rot=R3) # front window + mb.cyl(lp(0, 0.130, 0.0), lp(0, 0.1445, 0.0), 0.017, 'lens_dark', seg=24) # lens behind it + mb.cyl(lp(0, -0.145, -0.02), lp(0, -0.17, -0.02), 0.009, 'black_plastic', seg=10) # gland + # tilt mount between bracket and housing + mb.box(lp(0, 0.0, -0.055), (0.05, 0.08, 0.015), 'aluminum', rot=R3) + + +L.part('phenocam_mast', coll, root, build_mast) +L.part('phenocam_camera', coll, root, build_camera) +L.part('phenocam_buried', L.below(coll), root, + lambda mb: mb.cyl((0.25, 0.15, -0.8), (0.25, 0.15, 0.0), 0.008, 'copper', seg=8)) +cam = L.empty('phenocam_view', coll, root, tuple(lp(0, 0.15, 0)), 0.15) +cam.rotation_euler = (-TILT, 0, 0) # local +Y = boresight +for k, az in enumerate((30, 150, 270)): + a = math.radians(az) + anchor = Vector((3.0 * math.cos(a), 3.0 * math.sin(a), 0.0)) + L.part(f'phenocam_anchor_{k}', coll, root, lambda mb, p=anchor: ( + mb.cyl(p + Vector((0, 0, 0.0)), p + Vector((0, 0, 0.10)), 0.018, 'galvanized', seg=10),)) + L.wire(f'phenocam_guy_{k}', Vector((0.05 * math.cos(a), 0.05 * math.sin(a), 3.5)), + anchor + Vector((0, 0, 0.10)), 0.0025, coll, root) +gl = lp(0, -0.17, -0.02) +L.cable('phenocam_cable_poe', [tuple(gl), tuple(gl + Vector((0, -0.05, -0.08))), (0.0, ARM - 0.15, H - 0.04), + (0.0, 0.04, H - 0.11), (0.0, 0.04, H - 0.6), (0.0, 0.04, 2.5), + (0.04, -0.06, 1.55), (0.06, -0.12, 1.145)], 0.0035, coll, root) +L.cable('phenocam_cable_power', [(-0.06, -0.12, 1.145), (-0.06, -0.15, 0.7), (-0.08, -0.12, 0.10), + (-0.2, -0.45, 0.0), (-0.45, -0.8, 0.0)], 0.006, coll, root) + +L.finish(NAME, coll, root, + 'PhenoCam-style land-surface / phenology camera (StarDot NetCam SC 5MP class)', + ['M1', 'S1'], + ['PhenoCam Network FAQ (phenocam.nau.edu): StarDot NetCam SC 5.0 IR in housing with mounting bracket; ' + 'image dominated by the vegetation of interest, fixed field of view', + 'DERECHOS white paper section 4.2.4 (AI thrust 4: phenocams / site cameras) and errata (no Table 1 row)'], + f'5 m galvanised mast on a concrete pad, guyed at 3.5 m (az 30/150/270); {ARM:.2f} m side arm to +Y ' + f'with the camera housing looking north (+Y), {math.degrees(TILT):.0f} deg below the horizon ' + '(empty phenocam_view at the window). PoE/surge enclosure at 1.3 m facing -Y. Generic mast; ' + 'set the arm azimuth to the field the camera should frame.') +print('ALL DONE') diff --git a/tools/build_inst_soilvue10.py b/tools/build_inst_soilvue10.py new file mode 100644 index 0000000..7892a46 --- /dev/null +++ b/tools/build_inst_soilvue10.py @@ -0,0 +1,89 @@ +"""Campbell Scientific SoilVUE10 TDR soil profile probe -> instruments/SOILVUE10/SOILVUE10.blend +(collection INST_SOILVUE10). blender -b --factory-startup --python tools/build_inst_soilvue10.py + +DERECHOS section 3.1 advocates SoilVue10 profiling as close to (ideally in) the farm fields +at M1 and S1, and ATMOS/ANL run them (white paper Table 4). Specs (Campbell SoilVUE10 +spec sheet via NOAA PSL): 1 m model ~1.05 m long, 52 mm body (58 mm over the thread), +nine sensing depths 5-100 cm, installed vertically in a 5 cm augered hole with the top +flush with the soil surface. + +Model: the threaded probe below grade (helix approximated by thread rings, sensing-ring +marks at each depth), its cable in a shallow trench to a small logger enclosure (CR6-class) +on a T-post with a 20 W solar panel facing south. The logger stand is generic. +""" +import bpy, math, os, sys +from mathutils import Vector, Matrix +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'SOILVUE10' +coll, root = L.start(NAME) +LEN = 1.05 +DEPTHS = [0.05, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.75, 1.00] +POST = Vector((0.0, 1.5, 0.0)) # logger post 1.5 m north of the probe + + +def build_probe(mb): + """Below grade: the probe body, tip, threads and sensing rings.""" + mb.cyl((0, 0, -LEN), (0, 0, -0.005), 0.026, 'black_plastic', seg=24) + mb.cyl((0, 0, -LEN - 0.04), (0, 0, -LEN), 0.026, 'black_plastic', r1=0.004, seg=24) # tapered tip + z = -0.03 + while z > -LEN + 0.01: # thread crests + mb.cyl((0, 0, z), (0, 0, z - 0.006), 0.029, 'black_plastic', seg=24) + z -= 0.03 + for d in DEPTHS: # sensing rings + mb.cyl((0, 0, -d + 0.008), (0, 0, -d - 0.008), 0.0295, 'stainless', seg=24) + + +def build_top(mb): + """At the surface: flush cap, gland, tamped ring and trench scar.""" + mb.cyl((0, 0, -0.005), (0, 0, 0.004), 0.027, 'grey_plastic', seg=24) # top cap, flush + mb.cyl((0, 0, 0.004), (0, 0.0, 0.03), 0.007, 'black_plastic', seg=10) # cable gland + mb.cyl((0, 0, 0.0), (0, 0, 0.008), 0.12, 'soil_fresh', r1=0.07, seg=24) # tamped ring + mb.beam(Vector((0, 0.05, 0.002)), POST + Vector((0, -0.06, 0.002)), 0.10, 0.006, 'soil_fresh') # trench scar + + +def build_logger(mb): + p = POST + # steel T-post driven 0.6 m, enclosure and solar panel on U-bolts + mb.box(p + Vector((0, 0, 0.65)), (0.045, 0.012, 1.30), 'grey_paint') # above-ground T-post + mb.box(p + Vector((0, 0.012, 0.65)), (0.012, 0.035, 1.30), 'grey_paint') + mb.box(p + Vector((0, -0.10, 0.82)), (0.26, 0.15, 0.31), 'fiberglass_white') + mb.box(p + Vector((0, -0.178, 0.82)), (0.24, 0.006, 0.29), 'fiberglass_white') + mb.box(p + Vector((0, -0.182, 0.92)), (0.09, 0.002, 0.035), 'label') + for z in (0.70, 0.94): + mb.box(p + Vector((0, -0.015, z)), (0.18, 0.025, 0.03), 'galvanized') + mb.cyl(p + Vector((0.06, -0.10, 0.665)), p + Vector((0.06, -0.10, 0.64)), 0.010, 'grey_plastic', seg=10) + # 20 W panel tilted 45 deg to the south at the top of the post + tilt = Matrix.Rotation(math.radians(-45), 3, 'X') + c = p + Vector((0, -0.12, 1.33)) + mb.box(c, (0.50, 0.36, 0.03), 'aluminum', rot=tilt) + mb.box(c + tilt @ Vector((0, 0, 0.0155)), (0.47, 0.33, 0.002), 'solar_cell', rot=tilt) + mb.beam(p + Vector((0, -0.01, 1.25)), c + tilt @ Vector((0, 0, -0.02)), 0.03, 0.03, 'galvanized') + mb.beam(p + Vector((0, -0.01, 1.42)), c + tilt @ Vector((0, 0.10, -0.02)), 0.025, 0.025, 'galvanized') + + +L.part('soilvue10_probe', L.below(coll), root, build_probe) +L.part('soilvue10_top_cap', coll, root, build_top) +L.part('soilvue10_post_driven', L.below(coll), root, lambda mb: ( + mb.box(POST + Vector((0, 0, -0.30)), (0.045, 0.012, 0.60), 'grey_paint'), + mb.box(POST + Vector((0, 0.012, -0.30)), (0.012, 0.035, 0.60), 'grey_paint'), + mb.box(POST + Vector((0, 0.03, -0.25)), (0.10, 0.004, 0.18), 'grey_paint'))) +L.part('soilvue10_logger', coll, root, build_logger) +L.empty('soilvue10_top', coll, root, (0, 0, 0), 0.1) +L.cable('soilvue10_cable', [(0, 0, 0.02), (0, 0.06, 0.004), (0, 0.12, 0.0), (0, 0.8, 0.0), + tuple(POST + Vector((0.06, -0.20, 0.0))), tuple(POST + Vector((0.06, -0.12, 0.10))), + tuple(POST + Vector((0.06, -0.10, 0.64)))], 0.0035, coll, root) +L.cable('soilvue10_solar_lead', [tuple(POST + Vector((-0.20, -0.18, 1.22))), tuple(POST + Vector((-0.12, -0.10, 1.05))), + tuple(POST + Vector((-0.08, -0.10, 0.975)))], 0.003, coll, root) + +L.finish(NAME, coll, root, + 'Campbell Scientific SoilVUE10 TDR soil moisture and temperature profile probe (1 m)', + ['M1', 'S1', 'NIU'], + ['Campbell Scientific SoilVUE10 spec sheet (NOAA PSL copy): 1 m model ~1.05 m, 52/58 mm diameter, ' + 'depths 5,10,20,30,40,50,60,75,100 cm, top flush with the surface in a 5 cm auger hole', + 'DERECHOS white paper section 3.1 (SoilVue10 advocacy at M1/S1) and Table 4 (ATMOS SoilVUE10)'], + 'Probe top flush at the origin (empty soilvue10_top), body to -1.05 m in SOILVUE10_BelowGrade with stainless marks at the ' + 'nine sensing depths; cable in a shallow trench to a CR6-class enclosure on a steel T-post 1.5 m ' + 'north (+Y) with a 20 W panel tilted 45 deg south. Logger stand is generic.') +print('ALL DONE') diff --git a/tools/build_inst_stamp.py b/tools/build_inst_stamp.py new file mode 100644 index 0000000..b50961c --- /dev/null +++ b/tools/build_inst_stamp.py @@ -0,0 +1,141 @@ +"""ARM STAMP soil temperature and moisture profile station -> instruments/STAMP/STAMP.blend +(collection INST_STAMP). blender -b --factory-startup --python tools/build_inst_stamp.py + +From the STAMP handbook (DOE/SC-ARM-TR-186, D. R. Cook): three profiles (west, south, +east) about 1 m apart, each with Stevens HydraProbes at 5, 10, 20, 50 and 100 cm pushed +into the side of a hand-augered hole (15 probes); electronics (Campbell CR800 + +multiplexer + battery) in an enclosure on a pole in a concrete foundation placed away from +the probes; Texas Electronics TR-525M heated tipping-bucket gauge (245 mm knife-edge +collector, 305 mm tall). + +Everything below z = 0 (probes, cables in the holes, conduit) is modelled so cut-away or +x-ray views work; at the surface you see the backfilled auger holes, marker stakes, the +conduit trench, the enclosure pole and the rain gauge. Hole spacing and the 3 m offset to +the enclosure are generic. +""" +import bpy, math, os, sys +from mathutils import Vector, Matrix +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import instlib as L + +NAME = 'STAMP' +coll, root = L.start(NAME) +DEPTHS = [0.05, 0.10, 0.20, 0.50, 1.00] +# profiles 1 m apart, 3 m south of the enclosure pole; probes point away from the centre +PC = Vector((0, -3.0, 0)) +PROFILES = {'west': (PC + Vector((-1.0, 0.0, 0)), Vector((-1, 0, 0))), + 'south': (PC + Vector((0.0, -1.0, 0)), Vector((0, -1, 0))), + 'east': (PC + Vector((1.0, 0.0, 0)), Vector((1, 0, 0)))} +GAUGE = Vector((1.0, 0.3, 0)) + + +def build_holes(mb): + for name, (p, d) in PROFILES.items(): + mb.cyl(p + Vector((0, 0, 0.0)), p + Vector((0, 0, 0.012)), 0.16, 'soil_fresh', r1=0.10, seg=24) # mound + # marker stake with flag tape on the side away from the probes + s = p - d * 0.25 + mb.box(s + Vector((0, 0, 0.31)), (0.02, 0.02, 0.62), 'grey_paint') + mb.box(s + Vector((0, 0, 0.60)), (0.03, 0.03, 0.05), 'orange_flag') + + +def build_backfill(mb): + for name, (p, d) in PROFILES.items(): + mb.cyl(p + Vector((0, 0, -1.05)), p + Vector((0, 0, 0.0)), 0.05, 'soil_fresh', seg=20) + s = p - d * 0.25 + mb.box(s + Vector((0, 0, -0.15)), (0.02, 0.02, 0.30), 'grey_paint') # driven part of the stake + + +def hydraprobe(mb, p, d): + """Stevens HydraProbe: 42 mm x 124 mm body, 4 tines 57 mm, body axis along d.""" + body0 = p + d * 0.06 + body1 = body0 + d * 0.124 + mb.cyl(p, body0, 0.004, 'cable', seg=8) + mb.cyl(body0, body1, 0.021, 'white_plastic', seg=20) + mb.cyl(body1, body1 + d * 0.006, 0.021, 'stainless', seg=20) + side = Vector((-d.y, d.x, 0)) + tips = [Vector((0, 0, 0))] + [side * 0.011 * math.cos(a) + Vector((0, 0, 0.011 * math.sin(a))) + for a in (math.radians(90), math.radians(210), math.radians(330))] + for t in tips: + mb.cyl(body1 + t, body1 + t + d * 0.057, 0.0015, 'stainless', seg=6) + + +def build_probes(mb): + for name, (p, d) in PROFILES.items(): + for z in DEPTHS: + hydraprobe(mb, p + d * 0.04 + Vector((0, 0, -z)), d) + + +def build_station(mb): + """Everything at or above the surface.""" + mb.cyl((0, 0, 0.0), (0, 0, 0.04), 0.15, 'concrete', seg=24) # footing top + mb.cyl((0, 0, 0.0), (0, 0, 1.80), 0.0302, 'galvanized', seg=20) + mb.cyl((0, 0, 1.80), (0, 0, 1.82), 0.033, 'black_plastic', seg=20) + for z in (0.95, 1.55): + mb.box((0, -0.045, z), (0.40, 0.04, 0.04), 'galvanized') + mb.box((0, -0.03, z), (0.08, 0.03, 0.07), 'stainless') + mb.box((0, -0.18, 1.25), (0.42, 0.22, 0.56), 'fiberglass_white') + mb.box((0, -0.294, 1.25), (0.40, 0.008, 0.54), 'fiberglass_white') + mb.box((0, -0.299, 1.42), (0.12, 0.002, 0.05), 'label') + for x in (-0.18, 0.18): + for z in (1.03, 1.47): + mb.cyl((x, -0.298, z), (x, -0.308, z), 0.012, 'stainless', seg=10) + # PVC conduits rising into the enclosure bottom (signal, power) + mb.cyl((0.05, -0.18, 0.0), (0.05, -0.18, 0.97), 0.021, 'pvc_grey', seg=16) + mb.cyl((-0.08, -0.18, 0.0), (-0.08, -0.18, 0.97), 0.013, 'pvc_grey', seg=12) + mb.cyl((0.25, 0.15, 0.0), (0.25, 0.15, 0.08), 0.008, 'copper', seg=8) # ground rod top + # surface scar over the conduit trench to the profile centre + mb.beam(Vector((0.05, -0.45, 0.002)), PC + Vector((0, 0.05, 0.002)), 0.22, 0.008, 'soil_fresh') + # TR-525M heated rain gauge on its own pole, rim at 1.0 m + g = GAUGE + mb.cyl(g, g + Vector((0, 0, 0.04)), 0.12, 'concrete', seg=20) + mb.cyl(g, g + Vector((0, 0, 0.66)), 0.0302, 'galvanized', seg=20) + mb.box(g + Vector((0, 0, 0.68)), (0.22, 0.22, 0.012), 'galvanized') + rim = 0.1225 + mb.lathe([(0.11, 0.69), (0.123, 0.70), (0.128, 0.72), (0.128, 0.985), (rim + 0.002, 0.995), + (rim, 1.0), (rim - 0.002, 0.995), (0.118, 0.985), (0.03, 0.83), (0.012, 0.82)], + 'aluminum', M=Matrix.Translation(g), seg=48) + mb.box(g + Vector((0, -0.135, 0.76)), (0.06, 0.02, 0.04), 'black_plastic') # heater/connector + + +def build_station_buried(mb): + """Footings, buried pole ends, conduit at 0.3 m and the ground rod.""" + mb.cyl((0, 0, -0.60), (0, 0, 0.0), 0.15, 'concrete', seg=24) + mb.cyl((0, 0, -0.55), (0, 0, 0.0), 0.0302, 'galvanized', seg=20) + mb.cyl((0.05, -0.18, -0.30), (0.05, -0.18, 0.0), 0.021, 'pvc_grey', seg=16) + mb.cyl((0.05, -0.18, -0.30), (0.05, -0.60, -0.30), 0.021, 'pvc_grey', seg=16) + mb.cyl((-0.08, -0.18, -0.30), (-0.08, -0.18, 0.0), 0.013, 'pvc_grey', seg=12) + mb.cyl((0.25, 0.15, -0.8), (0.25, 0.15, 0.0), 0.008, 'copper', seg=8) + mb.cyl(Vector((0.05, -0.60, -0.30)), PC + Vector((0, 0.15, -0.30)), 0.021, 'pvc_grey', seg=16) + g = GAUGE + mb.cyl(g + Vector((0, 0, -0.50)), g, 0.12, 'concrete', seg=20) + mb.cyl(g + Vector((0, 0, -0.45)), g, 0.0302, 'galvanized', seg=20) + + +BG = L.below(coll) +L.part('stamp_surface', coll, root, build_holes) +L.part('stamp_station', coll, root, build_station) +L.part('stamp_backfill', BG, root, build_backfill) +L.part('stamp_hydraprobes', BG, root, build_probes) +L.part('stamp_station_buried', BG, root, build_station_buried) +for name, (p, d) in PROFILES.items(): + L.empty(f'stamp_profile_{name}', coll, root, tuple(p), 0.15) + # bundle of probe leads up the hole and into the trench + L.cable(f'stamp_leads_{name}', [tuple(p + Vector((0, 0, -1.0))), tuple(p + Vector((0, 0, -0.5))), + tuple(p + Vector((0, 0, -0.32))), tuple(p.lerp(PC, 0.6) + Vector((0, 0.05, -0.30))), + tuple(PC + Vector((0, 0.15, -0.30)))], 0.008, BG, root) +L.cable('stamp_gauge_cable', [tuple(GAUGE + Vector((0, -0.135, 0.74))), tuple(GAUGE + Vector((0, -0.04, 0.55))), + tuple(GAUGE + Vector((0, -0.035, 0.05))), (0.6, -0.1, 0.0), + (0.2, -0.2, 0.0), (0.05, -0.2, 0.30)], 0.005, coll, root) + +L.finish(NAME, coll, root, + 'Soil Temperature and Moisture Profiles (STAMP) station', + ['M1', 'S1'], + ['ARM STAMP handbook DOE/SC-ARM-TR-186 (D. R. Cook, 2018): 3 profiles ~1 m apart, HydraProbes at 5/10/20/50/100 cm, ' + 'CR800 enclosure on a pole in concrete, TR-525M heated gauge (245 mm collector, 305 mm tall)', + 'Stevens HydraProbe dimensions 42 x 124 mm, 57 mm tines', + 'DERECHOS white paper section 3.1 / errata: soil-moisture profiles requested at M1 and S1'], + 'Enclosure pole at the origin (door -Y), TR-525M gauge 1 m east/0.3 m north (rim 1.0 m AGL). ' + 'Profiles west/south/east 1 m from a centre 3 m south of the pole (empties stamp_profile_*), each ' + 'with five HydraProbes below ground pointing away from the centre; leads run in a buried PVC ' + 'conduit at 0.3 m. Underground parts are real geometry in STAMP_BelowGrade (hidden; un-hide for cut-aways).') +print('ALL DONE') diff --git a/tools/instlib.py b/tools/instlib.py new file mode 100644 index 0000000..f2d1f60 --- /dev/null +++ b/tools/instlib.py @@ -0,0 +1,335 @@ +"""Procedural builder helpers for new instrument assets (instruments//.blend). + +Run the per-instrument scripts headless from the repository root: + blender -b --factory-startup --python tools/build_inst_.py + +Conventions follow the extracted instrument library: + * one collection INST_, marked as an asset, not linked to any scene; + * a root empty _root at the origin (ARROWS, 0.5) that parents every part and + carries the custom properties instrument / full_name / sites / sources / notes / + height_m / bbox_min / bbox_max (lists stored as JSON strings); + * +Z up, +Y north, metres; the asset stands on z = 0. +Materials are appended (copied, never linked) from existing instrument files so new +assets share the library's PBR look; their textures stay relative +(//../../textures/pbr/...). Source files are only read. +""" +import bpy, bmesh, math, os, json +from mathutils import Vector, Matrix + +REPO = os.environ.get('ABLE_REPO') or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAT_SOURCES = ['instruments/MET/MET.blend', 'instruments/SIRS/SIRS.blend', + 'instruments/LDIS/LDIS.blend', 'instruments/ASI/ASI.blend'] +# simple materials (no textures) used when the library has nothing suitable +FLAT = { + 'soil_dark': ((0.11, 0.08, 0.055), 0.0, 0.95), + 'soil_fresh': ((0.17, 0.12, 0.08), 0.0, 0.9), + 'orange_flag': ((0.95, 0.30, 0.02), 0.0, 0.5), + 'radar_white': ((0.86, 0.87, 0.86), 0.0, 0.45), + 'solar_cell': ((0.02, 0.03, 0.07), 0.3, 0.15), + 'metek_grey': ((0.55, 0.57, 0.58), 0.0, 0.4), + 'stardot_grey': ((0.62, 0.63, 0.62), 0.2, 0.35), + 'kipp_white': ((0.88, 0.88, 0.86), 0.0, 0.35), + 'teros_blue': ((0.05, 0.18, 0.45), 0.0, 0.4), +} +_mat_index = None +_mats = {} + + +def log(*a): + print('[inst]', *a, flush=True) + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + global _mat_index + _mat_index = None + _mats.clear() + + +def _index(): + global _mat_index + if _mat_index is None: + _mat_index = {} + for rel in MAT_SOURCES: + p = os.path.join(REPO, rel) + with bpy.data.libraries.load(p, link=False) as (src, _): + names = list(src.materials) + for n in names: + _mat_index.setdefault(n.split('.')[0], (p, n)) + return _mat_index + + +def mat(base): + """Material by base name: appended from the library, else a flat Principled one.""" + if base in _mats: + return _mats[base] + idx = _index() + if base in idx: + p, n = idx[base] + with bpy.data.libraries.load(p, link=False) as (_, dst): + dst.materials = [n] + m = dst.materials[0] + m.name = base + else: + col, metal, rough = FLAT[base] + m = bpy.data.materials.new(base) + m.use_nodes = True + b = m.node_tree.nodes['Principled BSDF'] + b.inputs['Base Color'].default_value = (*col, 1) + b.inputs['Metallic'].default_value = metal + b.inputs['Roughness'].default_value = rough + _mats[base] = m + return m + + +def frame_z(p0, p1): + """Matrix whose +Z runs from p0 towards p1, origin at p0.""" + p0, p1 = Vector(p0), Vector(p1) + d = p1 - p0 + q = d.normalized().to_track_quat('Z', 'Y' if abs(d.normalized().z) < 0.99 else 'X') + return Matrix.Translation(p0) @ q.to_matrix().to_4x4(), d.length + + +class MB: + """bmesh accumulator: primitives in world (asset) coordinates, per-face materials.""" + + def __init__(self): + self.bm = bmesh.new() + self.mats = [] + + def _mi(self, m): + if m not in self.mats: + self.mats.append(m) + return self.mats.index(m) + + def _tag(self, geom, m, smooth): + mi = self._mi(m) + faces = set() + for v in geom: + if isinstance(v, bmesh.types.BMVert): + faces.update(v.link_faces) + elif isinstance(v, bmesh.types.BMFace): + faces.add(v) + for f in faces: + f.material_index = mi + f.smooth = smooth + return faces + + def box(self, c, size, m, rot=None): + M = Matrix.Translation(Vector(c)) @ (rot.to_4x4() if rot is not None else Matrix()) \ + @ Matrix.Diagonal((*size, 1)) + r = bmesh.ops.create_cube(self.bm, size=1.0, matrix=M) + self._tag(r['verts'], m, False) + + def beam(self, p0, p1, w, h, m): + M, L = frame_z(p0, p1) + M = M @ Matrix.Translation((0, 0, L / 2)) @ Matrix.Diagonal((w, h, L, 1)) + r = bmesh.ops.create_cube(self.bm, size=1.0, matrix=M) + self._tag(r['verts'], m, False) + + def cyl(self, p0, p1, r0, m, r1=None, seg=24, cap=True, smooth=True): + M, L = frame_z(p0, p1) + M = M @ Matrix.Translation((0, 0, L / 2)) + r = bmesh.ops.create_cone(self.bm, cap_ends=cap, cap_tris=False, segments=seg, + radius1=r0, radius2=r0 if r1 is None else r1, depth=L, matrix=M) + faces = self._tag(r['verts'], m, smooth) + for f in faces: # keep caps flat-shaded + if len(f.verts) > 4: + f.smooth = False + + def sphere(self, c, r, m, seg=16, scale=(1, 1, 1)): + M = Matrix.Translation(Vector(c)) @ Matrix.Diagonal((*scale, 1)) + res = bmesh.ops.create_uvsphere(self.bm, u_segments=seg, v_segments=max(6, seg // 2), + radius=r, matrix=M) + self._tag(res['verts'], m, True) + + def lathe(self, prof, m, M=Matrix(), seg=32, smooth=True): + """prof: [(r, z), ...] revolved about local Z; M places it.""" + rings = [] + for (r, z) in prof: + ring = [] + for i in range(seg): + a = 2 * math.pi * i / seg + ring.append(self.bm.verts.new(M @ Vector((r * math.cos(a), r * math.sin(a), z)))) + rings.append(ring) + new = [] + for a, b in zip(rings, rings[1:]): + for i in range(seg): + j = (i + 1) % seg + new.append(self.bm.faces.new((a[i], a[j], b[j], b[i]))) + if prof[0][0] > 1e-6: + new.append(self.bm.faces.new(list(reversed(rings[0])))) + if prof[-1][0] > 1e-6: + new.append(self.bm.faces.new(rings[-1])) + self._tag(new, m, smooth) + for f in new: + if len(f.verts) > 4: + f.smooth = False + + def grid(self, pts, m, smooth=True, close_u=False): + """pts[i][j] -> quads; close_u wraps index j.""" + vs = [[self.bm.verts.new(Vector(p)) for p in row] for row in pts] + new = [] + nj = len(vs[0]) + for i in range(len(vs) - 1): + for j in range(nj if close_u else nj - 1): + k = (j + 1) % nj + new.append(self.bm.faces.new((vs[i][j], vs[i][k], vs[i + 1][k], vs[i + 1][j]))) + self._tag(new, m, smooth) + return vs + + def obj(self, name, coll, parent): + me = bpy.data.meshes.new(name) + self.bm.normal_update() + self.bm.to_mesh(me) + self.bm.free() + for mm in self.mats: + me.materials.append(mat(mm)) + o = bpy.data.objects.new(name, me) + coll.objects.link(o) + o.parent = parent + return o + + +def part(name, coll, parent, build): + """build(mb) fills a fresh MB; returns the object.""" + mb = MB() + build(mb) + return mb.obj(name, coll, parent) + + +def cable(name, pts, r, coll, parent, m='cable'): + cu = bpy.data.curves.new(name, 'CURVE') + cu.dimensions = '3D' + cu.bevel_depth = r + cu.bevel_resolution = 2 + sp = cu.splines.new('NURBS') + sp.points.add(len(pts) - 1) + for p, q in zip(sp.points, pts): + p.co = (*q, 1) + sp.use_endpoint_u = True + sp.order_u = min(4, len(pts)) + sp.resolution_u = 8 + cu.materials.append(mat(m)) + o = bpy.data.objects.new(name, cu) + coll.objects.link(o) + o.parent = parent + return o + + +def wire(name, p0, p1, r, coll, parent, m='galvanized'): + """Straight guy wire / stay as a thin cylinder.""" + return part(name, coll, parent, lambda mb: mb.cyl(p0, p1, r, m, seg=6)) + + +def empty(name, coll, parent, loc=(0, 0, 0), size=0.2): + e = bpy.data.objects.new(name, None) + e.empty_display_type = 'PLAIN_AXES' + e.empty_display_size = size + e.location = loc + coll.objects.link(e) + e.parent = parent + return e + + +def start(name): + """New INST_ collection + _root.""" + reset() + coll = bpy.data.collections.new('INST_' + name) + root = bpy.data.objects.new(name + '_root', None) + root.empty_display_type = 'ARROWS' + root.empty_display_size = 0.5 + coll.objects.link(root) + return coll, root + + +def below(coll): + """Child collection for geometry below grade (probes, footings, buried cable). + Hidden in viewport and render so scenes, previews and bounds see only what stands on + the ground; un-hide it for cut-away views.""" + nm = coll.name.replace('INST_', '') + '_BelowGrade' + c = bpy.data.collections.get(nm) + if c is None: + c = bpy.data.collections.new(nm) + coll.children.link(c) + c.hide_viewport = True + c.hide_render = True + return c + + +def finish(name, coll, root, full_name, sites, sources, notes): + """Write props + bbox, mark asset, save instruments//.blend.""" + sc = bpy.data.scenes[0] if bpy.data.scenes else bpy.data.scenes.new('Scene') + sc.collection.children.link(coll) + # drop anything an append dragged in that is not part of the asset (e.g. old parents) + keep = set(coll.all_objects) + for o in list(bpy.data.objects): + if o not in keep: + log('drop stray', o.name) + bpy.data.objects.remove(o) + # bake curve cables to meshes: an instanced curve reports a stale unit-cube bound_box, + # which drops tools/render_previews.py's ground plane to z = -1 + for c in [coll] + list(coll.children_recursive): + c.hide_viewport = False # evaluate hidden below-grade parts too + bpy.context.view_layer.update() + dg = bpy.context.evaluated_depsgraph_get() + for o in [o for o in coll.all_objects if o.type == 'CURVE']: + me = bpy.data.meshes.new_from_object(o.evaluated_get(dg), depsgraph=dg) + me.name = o.name + new = bpy.data.objects.new(o.name + '_m', me) + new.parent = o.parent + new.matrix_parent_inverse = o.matrix_parent_inverse.copy() + new.matrix_basis = o.matrix_basis.copy() + for c in o.users_collection: + c.objects.link(new) + nm = o.name + cu = o.data + bpy.data.objects.remove(o) + bpy.data.curves.remove(cu) + new.name = nm + bgc = bpy.data.collections.get(name + '_BelowGrade') + if bgc is not None: + bgc.hide_viewport = True + bpy.data.orphans_purge(do_recursive=True) + # evaluate in a temporary scene so modifiers/curves count + bpy.context.view_layer.update() + dg = bpy.context.evaluated_depsgraph_get() + bg = bpy.data.collections.get(name + '_BelowGrade') + lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3); zbg = 0.0 + for o in coll.all_objects: + if o.type not in ('MESH', 'CURVE'): + continue + oe = o.evaluated_get(dg) + me = oe.to_mesh() # curve bound_box is stale; use real vertices + for v in me.vertices: + w = o.matrix_world @ v.co + if bg is not None and bg in o.users_collection: + zbg = min(zbg, w.z) # bbox describes what stands above ground + continue + lo = Vector(map(min, lo, w)); hi = Vector(map(max, hi, w)) + oe.to_mesh_clear() + if bg is not None: + root['below_grade_m'] = round(zbg, 3) + root['below_grade_collection'] = bg.name + root['instrument'] = name + root['full_name'] = full_name + root['sites'] = json.dumps(sites) + root['sources'] = json.dumps(sources) + root['notes'] = notes + root['height_m'] = round(hi.z, 3) + root['bbox_min'] = json.dumps([round(v, 3) for v in lo]) + root['bbox_max'] = json.dumps([round(v, 3) for v in hi]) + sc.collection.children.unlink(coll) + coll.asset_mark() + out = os.path.join(REPO, 'instruments', name, name + '.blend') + os.makedirs(os.path.dirname(out), exist_ok=True) + bpy.ops.wm.save_as_mainfile(filepath=out, compress=True) + bpy.ops.file.make_paths_relative() + for img in bpy.data.images: # all textures must resolve inside the repo + ap = bpy.path.abspath(img.filepath) + assert os.path.exists(ap), ('missing texture', img.filepath) + assert img.filepath.startswith('//'), ('absolute texture path', img.filepath) + bpy.ops.wm.save_mainfile(compress=True) + nobj = len(coll.all_objects) + log('SAVED', out, nobj, 'objects, height', root['height_m'], 'bbox', root['bbox_min'], root['bbox_max']) + return out diff --git a/tools/preview_jobs/instruments.json b/tools/preview_jobs/instruments.json index c448a0d..993d42f 100644 --- a/tools/preview_jobs/instruments.json +++ b/tools/preview_jobs/instruments.json @@ -122,6 +122,17 @@ ], "samples": 64 }, + { + "lib": "instruments/GNDRAD/GNDRAD.blend", + "coll": "INST_GNDRAD", + "out": "instruments/preview/GNDRAD.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64 + }, { "lib": "instruments/IRT/IRT.blend", "coll": "INST_IRT", @@ -177,6 +188,18 @@ ], "samples": 64 }, + { + "lib": "instruments/METWXT/METWXT.blend", + "coll": "INST_METWXT", + "out": "instruments/preview/METWXT.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64, + "tight": 1.15 + }, { "lib": "instruments/MFR/MFR.blend", "coll": "INST_MFR", @@ -210,6 +233,18 @@ ], "samples": 64 }, + { + "lib": "instruments/MRR/MRR.blend", + "coll": "INST_MRR", + "out": "instruments/preview/MRR.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64, + "tight": 1.25 + }, { "lib": "instruments/MWR3C/MWR3C.blend", "coll": "INST_MWR3C", @@ -221,6 +256,17 @@ ], "samples": 64 }, + { + "lib": "instruments/PHENOCAM/PHENOCAM.blend", + "coll": "INST_PHENOCAM", + "out": "instruments/preview/PHENOCAM.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64 + }, { "lib": "instruments/RWP/RWP.blend", "coll": "INST_RWP", @@ -254,6 +300,18 @@ ], "samples": 64 }, + { + "lib": "instruments/SOILVUE10/SOILVUE10.blend", + "coll": "INST_SOILVUE10", + "out": "instruments/preview/SOILVUE10.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64, + "tight": 1.25 + }, { "lib": "instruments/SONDE/SONDE.blend", "coll": "INST_SONDE", @@ -265,6 +323,18 @@ ], "samples": 64 }, + { + "lib": "instruments/STAMP/STAMP.blend", + "coll": "INST_STAMP", + "out": "instruments/preview/STAMP.jpg", + "view": "34", + "res": [ + 1024, + 768 + ], + "samples": 64, + "tight": 1.0 + }, { "lib": "instruments/VDIS/VDIS.blend", "coll": "INST_VDIS",