"""THE ACTUAL TIP — ダイヤモンドの先端と、一回の通過が実際に残すもの The earlier work asserted two numbers and never checked them against each other: a 0.10 mm groove, and 3 um removed per pass. They are not compatible. A conical tip cuts a groove whose width follows from its depth. For a 120 degree diamond, w = 2*d*tan(60) = 3.46*d, so a 3 um scratch is 10 um wide, not 100. To open a 100 um groove the tip has to be 29 um down — ten times deeper than assumed. The two numbers described different tools. Worse, the accumulation model let depth grow without bound toward a 140 um "saturation": forty passes reached 83 um, six thousand reached 140. That is not how a dragged stylus behaves. The depth of a plastic scratch is set by the LOAD, not by the number of passes. Once the tip has sunk to where the metal's flow pressure balances the spring, it stops, and the second pass drops into the groove the first one made and finds the same balance already satisfied. Cold work makes the steel harder, so later passes reach LESS far, not more. So this file computes, from the tip geometry and the spring load: * how deep one pass goes into annealed 304, * how wide the groove that leaves, * how much of the displaced metal actually leaves as chips rather than piling up along the edges, * and what repeated passes do, which is almost nothing. Geometry -------- The tip is a cone of included angle CONE_DEG, blunted to a spherical apex of radius TIP_R. Contact is spherical while the indentation is shallower than h0 = R(1-cos a), conical beyond it. For 120 deg and R = 12.5 um that crossover is at 6.25 um, which is inside the working range, so both regimes matter. Load balance ------------ A sliding indenter is supported by roughly the leading half of its contact area, so F = H * (pi/2) * a^2 with H the flow pressure (Meyer hardness). 304 in 2B mill finish is about 200 HV and saturates near 450 HV once cold worked. References for the tip: 120 degrees is the standard included angle for general purpose engraving on uncoated metal, with 90 degrees reserved for harder or anodised stock; drag engraving on stainless is known to leave shallower marks than on softer metals. Spring loads on drag holders are adjustable and are not usually published, so LOAD_GF is the one number here that is a choice rather than a specification, and every result below scales with it. """ from __future__ import annotations import math # --- the tip -------------------------------------------------------------- CONE_DEG = 120.0 # included angle of the diamond TIP_R_UM = 12.5 # radius of the blunted apex, um # THE GROOVE WIDTH IS THE SPECIFICATION, AND THE LOAD FOLLOWS FROM IT. # # The tool's groove is 0.05 to 0.15 mm wide. That is given, so it is the input # here, and the spring load is a DERIVED quantity — which is better, because a # load is a prediction that can be checked against the holder while a width can # be measured off the metal. # # The load balance fixes the CONTACT RADIUS (a = sqrt(2F / (pi H))), so the width # is what the load actually decides; the depth then follows from the tip geometry. # Reading the chain in this direction rather than the other one: # # width mm depth um w/d load gf groove area um2 r_fine mm % of disc # 0.05 12.50 4.00 196 265 25.5 1.6 # 0.10 26.93 3.71 785 806 50.9 6.8 # 0.15 41.37 3.63 1767 1709 76.4 15.3 # # The default is the middle of the specified range, and that is also the figure # every trajectory in this repository has already used (scribe.GROOVE), so the # cut model and the runs finally describe the same tool. They did not before: the # runs assumed 0.10 mm while the cut model derived 0.0618 mm from a 300 gf guess, # and that discrepancy had to be carried as a caveat on the published page. GROOVE_MM = 0.10 # SPECIFIED. Range 0.05-0.15. GROOVE_SPEC_MM = (0.05, 0.15) # --- the metal ------------------------------------------------------------ HV_ANNEALED = 200.0 # 304 stainless, 2B mill finish HV_SATURATED = 450.0 # after heavy cold work HARDEN_PASSES = 3.0 # passes for the hardening to be mostly done PLOUGH_CHIP = 0.35 # fraction of displaced metal that leaves as chips PILEUP_WIDTH = 0.45 # ridge width, as a fraction of the groove's PILEUP_CREST = 0.30 # where the crest sits across that width, from the rim STOCK_MM = 1.5 # sheet thickness G = 9.80665 # m/s^2 HALF = math.radians(CONE_DEG * 0.5) TAN_A = math.tan(HALF) SIN_A = math.sin(HALF) COS_A = math.cos(HALF) # Where the blunting sphere is TANGENT to the cone. HALF is measured from the # AXIS (60 deg for a 120 deg included cone), so the tangency condition gives # A0 = R cos(HALF), h0 = R (1 - sin(HALF)) # and NOT R sin / R (1-cos), which are the same formulas with the angle taken # from the surface instead — i.e. the tangency of a 60 deg included cone. Using # those put a 30 degree kink at the junction (the profile was continuous but not # smooth) and, because the load fixes the contact radius rather than the depth, # reported the groove 1.93 um too deep. A0_UM = TIP_R_UM * COS_A # contact radius where sphere meets cone H0_UM = TIP_R_UM * (1.0 - SIN_A) # that point's height above the nadir def _load_gf_for_width(width_mm, hv): """Spring load that opens a groove of this width, in grams-force. The sliding load balance is F = H (pi/2) a^2 with a the contact half-width, so a — and therefore the WIDTH — is what the load really sets. Inverting it turns the specification into a checkable prediction about the holder. """ a_m = width_mm * 1e-3 * 0.5 return (hv * G * 1e6) * math.pi * 0.5 * a_m * a_m / (1e-3 * G) # Derived from the specified width, not chosen. Everything downstream still reads # LOAD_GF, so the pipeline follows the specification with no call site changed. LOAD_GF = _load_gf_for_width(GROOVE_MM, HV_ANNEALED) def hv_to_pa(hv): """Vickers number to flow pressure. 1 HV = 1 kgf/mm^2.""" return hv * G * 1e6 def load_n(gf=LOAD_GF): return gf * 1e-3 * G def contact_radius_um(depth_um): """Contact half-width of the tip at a given indentation depth.""" if depth_um <= 0.0: return 0.0 if depth_um <= H0_UM: return math.sqrt(max(0.0, 2.0 * TIP_R_UM * depth_um - depth_um * depth_um)) return A0_UM + (depth_um - H0_UM) * TAN_A def depth_for_contact_um(a_um): """Inverse of contact_radius_um.""" if a_um <= 0.0: return 0.0 if a_um <= A0_UM: return TIP_R_UM - math.sqrt(max(0.0, TIP_R_UM * TIP_R_UM - a_um * a_um)) return H0_UM + (a_um - A0_UM) / TAN_A def depth_for_load_um(hv, gf=LOAD_GF): """How far the tip sinks before the metal's flow pressure balances it. The leading half of the contact carries the load while sliding, so F = H * (pi/2) * a^2. Solve for a, then invert the tip geometry. """ a2 = 2.0 * load_n(gf) / (math.pi * hv_to_pa(hv)) # m^2 return depth_for_contact_um(math.sqrt(a2) * 1e6) def groove_width_um(depth_um): return 2.0 * contact_radius_um(depth_um) def for_width(width_mm): """Everything the tip does at a given specified groove width.""" d = depth_for_contact_um(width_mm * 1000.0 * 0.5) return { "width_mm": width_mm, "depth_um": d, "width_over_depth": width_mm * 1000.0 / d, "load_gf": _load_gf_for_width(width_mm, HV_ANNEALED), "groove_area_um2": groove_area_um2(d), "pileup_height_um": pileup_height_um(d), "chips_mm3_per_m": removed_mm3_per_m(d), "fraction_of_stock": d / (STOCK_MM * 1000.0), } def hv_after(passes): """Cold work saturates the surface. Hardness rises with strain, not time.""" if passes <= 0: return HV_ANNEALED f = 1.0 - math.exp(-passes / HARDEN_PASSES) return HV_ANNEALED + (HV_SATURATED - HV_ANNEALED) * f def pass_depths_um(n, gf=LOAD_GF): """Depth after each of n passes over the same line. Each pass can only reach the depth its load and the current hardness allow. The groove is already there, so if that reach is not deeper than what exists, the pass removes nothing and only work-hardens what it rubs. """ out = [] d = 0.0 for i in range(n): reach = depth_for_load_um(hv_after(i), gf) d = max(d, reach) out.append(d) return out def groove_area_um2(depth_um): """Cross-section of the void, um^2. Spherical cap or truncated wedge.""" if depth_um <= 0.0: return 0.0 if depth_um <= H0_UM: # circular segment of radius R and height depth R = TIP_R_UM t = max(-1.0, min(1.0, (R - depth_um) / R)) th = math.acos(t) return R * R * (th - math.sin(2.0 * th) * 0.5) a = contact_radius_um(depth_um) R = TIP_R_UM th = HALF cap = R * R * (th - math.sin(2.0 * th) * 0.5) # trapezoid between the crossover and the surface return cap + (A0_UM + a) * (depth_um - H0_UM) * 0.5 def removed_mm3_per_m(depth_um): """Chip volume per metre of groove, mm^3.""" return groove_area_um2(depth_um) * 1e-6 * 1000.0 * PLOUGH_CHIP def pileup_height_um(depth_um): """Height of the ridge thrown up along each edge. What the chips do not carry away has to go somewhere, and on a ploughing scratch it goes sideways. Conserve the leftover area across two ridges of width PILEUP_WIDTH*w, taken as triangles. """ left = groove_area_um2(depth_um) * (1.0 - PLOUGH_CHIP) w = groove_width_um(depth_um) ridge_w = max(1e-9, PILEUP_WIDTH * w) # two triangles: 2 * (1/2 * ridge_w * h) = ridge_w * h return left / ridge_w def spec(gf=LOAD_GF): d1 = depth_for_load_um(HV_ANNEALED, gf) ds = pass_depths_um(40, gf) return { "cone_deg": CONE_DEG, "tip_radius_um": TIP_R_UM, "load_gf": gf, "sphere_cone_crossover_um": round(H0_UM, 3), "hv_annealed": HV_ANNEALED, "hv_saturated": HV_SATURATED, "first_pass_depth_um": round(d1, 3), "first_pass_width_um": round(groove_width_um(d1), 2), "depth_after_40_passes_um": round(ds[-1], 3), "extra_depth_from_39_more_passes_um": round(ds[-1] - ds[0], 4), "reach_when_saturated_um": round(depth_for_load_um(HV_SATURATED, gf), 3), "groove_area_um2": round(groove_area_um2(d1), 1), "chip_fraction": PLOUGH_CHIP, "removed_mm3_per_m": round(removed_mm3_per_m(d1), 5), "pileup_height_um": round(pileup_height_um(d1), 3), "aspect_width_over_depth": round(groove_width_um(d1) / d1, 2), } if __name__ == "__main__": import json print(json.dumps(spec(), indent=1)) print() print("load depth width w/d chips per metre") for gf in (100, 150, 200, 300, 400, 600, 800): d = depth_for_load_um(HV_ANNEALED, gf) print("%4d gf %5.1f um %5.1f um %4.1f %7.4f mm3" % (gf, d, groove_width_um(d), groove_width_um(d) / d, removed_mm3_per_m(d))) print() print("what repeated passes over one line actually do (at %.0f gf):" % LOAD_GF) ds = pass_depths_um(10) for i, d in enumerate(ds): print(" pass %2d: reach %5.2f um (hardness %3.0f HV) -> depth %5.2f um" % (i + 1, depth_for_load_um(hv_after(i)), hv_after(i), d)) print() print("the old model, for comparison: 0.10 mm groove with 3 um per pass") print(" a 120 deg cone 3 um down is %.1f um wide, not 100" % groove_width_um(3.0)) print(" opening a 100 um groove needs a depth of %.1f um" % depth_for_contact_um(50.0)) print(" and a load of %.0f gf" % (hv_to_pa(HV_ANNEALED) * math.pi * 0.5 * (50e-6) ** 2 / (1e-3 * G)))