diff --git a/README.md b/README.md index 3ccd719..d96d094 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ Generate `_cso_bindings` from the authored functions before importing them; define, generate, import and call workflow. The [section-property comparisons](examples/section-properties/README.md#reuse-in-a-comparison) apply it to larger calculations while retaining their documented intermediates. +The [cylindrical tank estimate](examples/cylinder-tank/README.md) uses the same +method for unit conversion, integer counts, conditional fill, plan offsets, and +a repeated shell comparison. ## Use Python's scientific libraries diff --git a/examples/cylinder-tank/README.md b/examples/cylinder-tank/README.md new file mode 100644 index 0000000..04035a1 --- /dev/null +++ b/examples/cylinder-tank/README.md @@ -0,0 +1,123 @@ +# Cylindrical tank estimate + +These calculations estimate the steel in an open vertical cylinder, the liquid it +contains, and the plan position of its centre. They are a material and contents +estimate, not a structural capacity calculation. Human engineering approval is +not claimed. + +The sources are separate calculation shapes. Each one exercises a rule from +[authoring](../../docs/authoring.md) that the other maintained examples do not +combine in a small project. + +| Shape | Source | What is different | +| --- | --- | --- | +| Shared metadata | [quantities.py](quantities.py) | Float and int aliases for glyphs, descriptions, and units | +| Reusable circle in a nested path | [geometry/circle.cso.py](geometry/circle.cso.py) | `math.pi`, a hidden radius, and qualified glyphs. The directory name is a Python identifier | +| Explicit unit conversion | [thickness.cso.py](thickness.cso.py) | Millimetres become metres in their own calculation. Callers do not convert by ignoring the unit | +| Integer counts | [courses.cso.py](courses.cso.py) | `int` inputs and `int` results, including `math.ceil` and `max`. The maximum course height is a callee default | +| Conditional fill | [contents.cso.py](contents.cso.py) | A comparison chain selects empty, partial, or full liquid height. Only the selected branch is evaluated | +| Notation scopes and trigonometry | [position.cso.py](position.cso.py) | `c_{off}` is kept in `x-axis` and `y-axis` scopes. Degrees are converted with `math.radians` before `cos` and `sin`; `hypot` and `atan2` recover the radius and bearing | +| Composition and a hidden intermediate | [shell.cso.py](shell.cso.py) | Calls the circle, conversion, and course calculations. A `document_section` groups the plate formulas. Developed area is documented and is not a public output | +| Parent estimate | [estimate.cso.py](estimate.cso.py) | Forwards the inside area into the contents calculation, so that quantity keeps its identity. Adds assumptions, a notation legend, and [tank.svg](tank.svg) | +| Repeated calls | [compare.cso.py](compare.cso.py) | Calls the shell twice. `baseline_shell` and `candidate_shell` qualify repeated glyphs. The parent returns the masses and the ratio; the child formulas stay in the document | +| Reference-glyph reproduction | [references/published_circle.cso.py](references/published_circle.cso.py) | The tank does not call this file. It preserves the published glyphs `d`, `r`, `C`, and `A` and records the reference URL | + +## Generate and verify + +Run from the repository root after [setup](../../docs/development.md#setup). +`PYTHON` selects the interpreter that contains the installed `cs-object` wheel. + +```sh +"$PYTHON" -m cso_python bindings examples/cylinder-tank +node packages/cso-cli/dist/cli.js bindings examples/cylinder-tank --check +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/estimate.cso.py --function estimate --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/estimate.cso.py --function estimate --input fill_ratio=0 --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/estimate.cso.py --function estimate --input fill_ratio=1 --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/estimate.cso.py --function estimate --input bearing_degrees=90 --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/estimate.cso.py --function estimate --input course_count=3 --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/compare.cso.py --function compare_shells --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/compare.cso.py --function compare_shells --input candidate_diameter=2 --reference examples/cylinder-tank/reference.json --format json +node packages/cso-cli/dist/cli.js verify examples/cylinder-tank/references/published_circle.cso.py --function published_circle --reference examples/cylinder-tank/reference.json --format json +``` + +For a plain Python consumer, use this directory as the working folder: + +```sh +(cd examples/cylinder-tank && "$PYTHON" - <<'PY' +import math +from _cso_bindings.estimate import estimate +from _cso_bindings.compare import compare_shells + +result = estimate() +assert result["area"] == math.pi +assert result["bolt_count"] == 48 +assert result["radial_offset"] == 5.0 +assert compare_shells()["mass_ratio"] == 1.25 +assert compare_shells(candidate_diameter=2)["mass_ratio"] == 1.0 +print(result) +PY +) +``` + +Successful execution does not establish source-to-document consistency. `verify` +does. Passing `--reference` adds independent numerical agreement. HTML and PDF +checks add content retention, and PDF adds rendering. Visual inspection of every +page is still separate. See [rendering](../../docs/rendering.md#choose-verification-by-change). + +## Expected results + +The default estimate uses diameter 2 m, height 6 m, wall 8 mm, steel density +7850 kg/m^3, 4 courses, 12 bolts per course, fill ratio 0.5, liquid density +1000 kg/m^3, plan radius 5 m, and bearing 0 degrees. The maximum course height +stays at its callee default of 1.5 m. + +| Quantity | Result | +| --- | --- | +| Inside radius, area, circumference | 1 m, pi m^2, 2 pi m | +| Wall thickness | 0.008 m | +| Developed area, shell volume, shell mass | 12 pi m^2, 0.096 pi m^3, 753.6 pi kg | +| Course height, required courses, bolts, shortfall | 1.5 m, 4, 48, 0 | +| Liquid height, volume, mass, freeboard | 3 m, 3 pi m^3, 3000 pi kg, 3 m | +| East, north, radial offset, recovered bearing | 5 m, 0 m, 5 m, 0 degrees | + +`pi` here is Python's `math.pi`. Products that are not exact decimals are the +binary64 results of the authored operation order. At these defaults the shell +mass is 2367.504223745268 kg and the liquid mass is 9424.77796076938 kg. + +Fill ratio 0 selects the empty branch: liquid height, volume, and mass are 0, +and freeboard is 6 m. Fill ratio 1 selects the full branch: liquid height is +6 m, liquid volume is 6 pi m^3, liquid mass is 6000 pi kg, and freeboard is 0. +Bearing 90 degrees gives a north component of 5 m, a radial offset of 5 m, and +a recovered bearing of 90 degrees. The east component is the binary64 cosine of +a right angle, about 3.06e-16 m, not a mathematical zero. Three courses give a +course height of 2 m, 36 bolts, and a shortfall of 1. + +The default comparison uses diameters 2 m and 2.5 m. Shell mass is proportional +to diameter, and the candidate-to-baseline ratio is 1.25. Equal diameters give +a ratio of 1. The published circle at diameter 2 m has radius 1 m, circumference +2 pi m, and area pi m^2. + +[reference.json](reference.json) binds these values to the source bytes, the +selected function, and the resolved inputs. Formula-only edits leave generated +handles current and make this file stale. Review the formulas and source hashes +before rebinding it. Do not copy expected numbers from the execution being +checked. + +## Notation + +Python names stay descriptive. Display glyphs are compact, and the estimate +legend defines them: tank, circ, wall, mm, stl, liq, crs, bolt, req, short, +free, plan, bear, rad, off, rec, dev, and shl. + +`c_{off}` is one glyph in two scopes. The x-axis scope is the easting and the +y-axis scope is the northing. The diagram uses `D_{tank}`, `h_{tank}`, and +`t_{wall,mm}`. + +The comparison qualifies repeated results. `baseline_shell` and +`candidate_shell` become `bs` and `cs`. Nested `base_circle`, `wall_thickness`, +and `course_bolts` become `bc`, `wt`, and `cb`. The baseline inside area is +`A_{circ,bs,bc}` and the baseline shell mass is `m_{shl,bs}`. Equal inputs do +not merge the two shells. + +The published circle is the exception that keeps bare reference glyphs. The tank +sources do not use those glyphs. diff --git a/examples/cylinder-tank/compare.cso.py b/examples/cylinder-tank/compare.cso.py new file mode 100644 index 0000000..7efbf8b --- /dev/null +++ b/examples/cylinder-tank/compare.cso.py @@ -0,0 +1,65 @@ +"""Compare two cylindrical shells that share every input except diameter.""" + +from typing import Annotated + +from _cso_bindings.shell import shell +from cso_python import CalculationResults, calculation, section, symbol, text +from quantities import ( + BaselineDiameter, + BoltsPerCourse, + CandidateDiameter, + CourseCount, + SteelDensity, + TankHeight, + WallThicknessMillimetres, +) + + +@calculation(id="compare-shells", title="Compare cylindrical shells") +@section(id="comparison", title="Shell comparison", root=True) +def compare_shells( + baseline_diameter: BaselineDiameter = 2.0, + candidate_diameter: CandidateDiameter = 2.5, + height: TankHeight = 6.0, + thickness_mm: WallThicknessMillimetres = 8.0, + density: SteelDensity = 7850.0, + course_count: CourseCount = 4, + bolts_per_course: BoltsPerCourse = 12, +) -> CalculationResults: + text( + id="scope", + content="Both shells use the same height, wall thickness, density, and course schedule. Only the inside diameter changes. The mass ratio describes that geometry. It does not establish capacity or compliance. Baseline diameter, height, thickness, and density must be positive.", + ) + text( + id="notation", + content="Subscripts base and cand identify the input diameters. Result qualifiers bs and cs identify baseline_shell and candidate_shell. Nested qualifiers bc, wt, and cb identify base_circle, wall_thickness, and course_bolts.", + ) + baseline_shell = shell( + diameter=baseline_diameter, + height=height, + thickness_mm=thickness_mm, + density=density, + course_count=course_count, + bolts_per_course=bolts_per_course, + ) + candidate_shell = shell( + diameter=candidate_diameter, + height=height, + thickness_mm=thickness_mm, + density=density, + course_count=course_count, + bolts_per_course=bolts_per_course, + ) + mass_ratio: Annotated[ + float, + symbol( + glyph=r"R_{mass}", + description="Candidate-to-baseline shell mass ratio", + unit="", + ), + ] = candidate_shell["shell_mass"] / baseline_shell["shell_mass"] + return { + "baseline_mass": baseline_shell["shell_mass"], + "candidate_mass": candidate_shell["shell_mass"], + "mass_ratio": mass_ratio, + } diff --git a/examples/cylinder-tank/contents.cso.py b/examples/cylinder-tank/contents.cso.py new file mode 100644 index 0000000..e17188b --- /dev/null +++ b/examples/cylinder-tank/contents.cso.py @@ -0,0 +1,47 @@ +"""Liquid contents of a vertical cylindrical tank.""" + +from typing import Annotated + +from cso_python import CalculationResults, calculation, section, symbol, text +from quantities import CircleArea, FillRatio, LiquidDensity, TankHeight + + +@calculation(id="tank-contents", title="Tank contents") +@section(id="contents", title="Liquid contents") +def contents( + base_area: CircleArea, + height: TankHeight, + fill_ratio: FillRatio = 0.5, + liquid_density: LiquidDensity = 1000.0, +) -> CalculationResults: + text( + id="fill", + content="A fill ratio at or below zero is empty, a ratio at or above one is full, and a ratio between them is a partial fill. liq = liquid; free = freeboard.", + ) + liquid_height: Annotated[ + float, + symbol(glyph=r"h_{liq}", description="Liquid height", unit="m"), + ] = ( + height * fill_ratio + if 0 < fill_ratio < 1 + else height + if fill_ratio >= 1 + else 0.0 + ) + liquid_volume: Annotated[ + float, + symbol(glyph=r"V_{liq}", description="Liquid volume", unit="m^3"), + ] = base_area * liquid_height + liquid_mass: Annotated[ + float, + symbol(glyph=r"m_{liq}", description="Liquid mass", unit="kg"), + ] = liquid_volume * liquid_density + freeboard: Annotated[ + float, + symbol(glyph=r"h_{free}", description="Unfilled shell height", unit="m"), + ] = max(height - liquid_height, 0.0) + return { + "liquid_volume": liquid_volume, + "liquid_mass": liquid_mass, + "freeboard": freeboard, + } diff --git a/examples/cylinder-tank/courses.cso.py b/examples/cylinder-tank/courses.cso.py new file mode 100644 index 0000000..8a17ffc --- /dev/null +++ b/examples/cylinder-tank/courses.cso.py @@ -0,0 +1,42 @@ +"""Integer course and bolt counts for a cylindrical shell.""" + +import math +from typing import Annotated + +from cso_python import CalculationResults, calculation, section, symbol +from quantities import BoltsPerCourse, CourseCount, MaximumCourseHeight, TankHeight + + +@calculation(id="shell-courses", title="Shell course schedule") +@section(id="courses", title="Shell courses") +def shell_courses( + course_count: CourseCount, + bolts_per_course: BoltsPerCourse, + height: TankHeight, + maximum_course_height: MaximumCourseHeight = 1.5, +) -> CalculationResults: + required_course_count: Annotated[ + int, + symbol( + glyph=r"n_{req}", + description="Courses required by the maximum course height", + unit="", + ), + ] = math.ceil(height / maximum_course_height) + bolt_count: Annotated[ + int, + symbol(glyph=r"n_{bolt}", description="Bolts in the shell", unit=""), + ] = course_count * bolts_per_course + course_shortfall: Annotated[ + int, + symbol( + glyph=r"n_{short}", + description="Extra courses needed to respect the maximum course height", + unit="", + ), + ] = max(required_course_count - course_count, 0) + return { + "required_course_count": required_course_count, + "bolt_count": bolt_count, + "course_shortfall": course_shortfall, + } diff --git a/examples/cylinder-tank/estimate.cso.py b/examples/cylinder-tank/estimate.cso.py new file mode 100644 index 0000000..bac0a9a --- /dev/null +++ b/examples/cylinder-tank/estimate.cso.py @@ -0,0 +1,73 @@ +"""Cylindrical tank estimate: shell steel, liquid contents, and plan position.""" + +from _cso_bindings.contents import contents +from _cso_bindings.position import plan_offset +from _cso_bindings.shell import shell +from cso_python import CalculationResults, calculation, figure, section, text +from quantities import ( + BearingDegrees, + BoltsPerCourse, + CourseCount, + FillRatio, + LiquidDensity, + PlanRadius, + SteelDensity, + TankDiameter, + TankHeight, + WallThicknessMillimetres, +) + + +@calculation(id="cylinder-tank", title="Cylindrical tank estimate") +@section(id="summary", title="Inputs and totals", root=True) +def estimate( + diameter: TankDiameter = 2.0, + height: TankHeight = 6.0, + thickness_mm: WallThicknessMillimetres = 8.0, + density: SteelDensity = 7850.0, + course_count: CourseCount = 4, + bolts_per_course: BoltsPerCourse = 12, + fill_ratio: FillRatio = 0.5, + liquid_density: LiquidDensity = 1000.0, + plan_radius: PlanRadius = 5.0, + bearing_degrees: BearingDegrees = 0.0, +) -> CalculationResults: + text( + id="assumptions", + content="Open vertical cylinder with a uniform wall. Roof and floor plates, openings, corrosion allowance, and waste are excluded. Liquid volume uses the inside base area and the liquid height. This is a material and contents estimate, not a structural capacity calculation.", + ) + text( + id="notation", + content="Subscripts: tank = tank; circ = circle; wall = wall; mm = millimetres; stl = steel; liq = liquid; crs = course; bolt = bolt; req = required; short = shortfall; free = freeboard; plan = plan; bear = bearing; rad = radians; off = offset; rec = recovered; dev = developed; shl = shell. The centre offset c_off is shared: x-axis is easting and y-axis is northing. Circle formulas follow https://en.wikipedia.org/wiki/Circle with these qualified glyphs.", + ) + figure( + id="tank", + path="tank.svg", + media_type="image/svg+xml", + caption="Vertical cylindrical tank with inside diameter D_tank, shell height h_tank, and wall thickness t_wall,mm.", + alt="Vertical cylinder marked with inside diameter D_tank, shell height h_tank, and wall thickness t_wall,mm.", + ) + + shell_quantities = shell( + diameter=diameter, + height=height, + thickness_mm=thickness_mm, + density=density, + course_count=course_count, + bolts_per_course=bolts_per_course, + ) + liquid = contents( + base_area=shell_quantities["area"], + height=height, + fill_ratio=fill_ratio, + liquid_density=liquid_density, + ) + centre = plan_offset(plan_radius=plan_radius, bearing_degrees=bearing_degrees) + + return { + "area": shell_quantities["area"], + "shell_mass": shell_quantities["shell_mass"], + "liquid_mass": liquid["liquid_mass"], + "bolt_count": shell_quantities["bolt_count"], + "radial_offset": centre["radial_offset"], + } diff --git a/examples/cylinder-tank/geometry/circle.cso.py b/examples/cylinder-tank/geometry/circle.cso.py new file mode 100644 index 0000000..42db3b5 --- /dev/null +++ b/examples/cylinder-tank/geometry/circle.cso.py @@ -0,0 +1,26 @@ +"""Inside circle of a vertical cylindrical tank.""" + +import math +from typing import Annotated + +from cso_python import CalculationResults, calculation, section, symbol +from quantities import CircleArea, CircleCircumference, TankDiameter + + +@calculation( + id="tank-circle", + title="Tank base circle", + metadata={ + "formulaSource": "https://en.wikipedia.org/wiki/Circle", + "notation": "Qualified glyphs for a new calculation. circ means circle.", + }, +) +@section(id="circle", title="Base circle") +def circle(diameter: TankDiameter) -> CalculationResults: + radius: Annotated[ + float, + symbol(glyph=r"r_{circ}", description="Inside radius", unit="m"), + ] = diameter / 2 + area: CircleArea = math.pi * radius**2 + circumference: CircleCircumference = math.pi * diameter + return {"area": area, "circumference": circumference} diff --git a/examples/cylinder-tank/position.cso.py b/examples/cylinder-tank/position.cso.py new file mode 100644 index 0000000..8cc2102 --- /dev/null +++ b/examples/cylinder-tank/position.cso.py @@ -0,0 +1,67 @@ +"""Plan position of the tank centre.""" + +import math +from typing import Annotated + +from cso_python import CalculationResults, calculation, section, symbol, text +from quantities import BearingDegrees, PlanRadius + + +@calculation(id="plan-offset", title="Plan position") +@section(id="position", title="Plan position") +def plan_offset( + plan_radius: PlanRadius = 5.0, + bearing_degrees: BearingDegrees = 0.0, +) -> CalculationResults: + text( + id="scopes", + content="The centre offset uses one glyph, c_off. The x-axis scope is the easting and the y-axis scope is the northing. Bearings start at the east axis and turn toward north. rad = radians; rec = recovered.", + ) + bearing_radians: Annotated[ + float, + symbol( + glyph=r"\theta_{rad}", + description="Plan bearing converted to radians", + unit="rad", + ), + ] = math.radians(bearing_degrees) + east_offset: Annotated[ + float, + symbol( + glyph=r"c_{off}", + description="East component of the tank centre. The x-axis scope is the easting.", + unit="m", + notation_scope="x-axis", + ), + ] = plan_radius * math.cos(bearing_radians) + north_offset: Annotated[ + float, + symbol( + glyph=r"c_{off}", + description="North component of the tank centre. The y-axis scope is the northing.", + unit="m", + notation_scope="y-axis", + ), + ] = plan_radius * math.sin(bearing_radians) + radial_offset: Annotated[ + float, + symbol( + glyph=r"r_{off}", + description="Radial offset of the tank centre", + unit="m", + ), + ] = math.hypot(east_offset, north_offset) + recovered_bearing: Annotated[ + float, + symbol( + glyph=r"\theta_{rec}", + description="Bearing recovered from the east and north components", + unit="deg", + ), + ] = math.degrees(math.atan2(north_offset, east_offset)) + return { + "east_offset": east_offset, + "north_offset": north_offset, + "radial_offset": radial_offset, + "recovered_bearing": recovered_bearing, + } diff --git a/examples/cylinder-tank/quantities.py b/examples/cylinder-tank/quantities.py new file mode 100644 index 0000000..ab270bd --- /dev/null +++ b/examples/cylinder-tank/quantities.py @@ -0,0 +1,82 @@ +"""Shared presentation metadata for the cylindrical tank estimate.""" + +from typing import Annotated, TypeAlias + +from cso_python import symbol + +TankDiameter: TypeAlias = Annotated[ + float, + symbol(glyph=r"D_{tank}", description="Tank inside diameter", unit="m"), +] +TankHeight: TypeAlias = Annotated[ + float, + symbol(glyph=r"h_{tank}", description="Tank shell height", unit="m"), +] +WallThicknessMillimetres: TypeAlias = Annotated[ + float, + symbol( + glyph=r"t_{wall,mm}", + description="Shell wall thickness", + unit="mm", + ), +] +WallThicknessMetres: TypeAlias = Annotated[ + float, + symbol(glyph=r"t_{wall}", description="Shell wall thickness", unit="m"), +] +SteelDensity: TypeAlias = Annotated[ + float, + symbol(glyph=r"\rho_{stl}", description="Steel density", unit="kg/m^3"), +] +CircleArea: TypeAlias = Annotated[ + float, + symbol(glyph=r"A_{circ}", description="Inside base area", unit="m^2"), +] +CircleCircumference: TypeAlias = Annotated[ + float, + symbol(glyph=r"C_{circ}", description="Inside circumference", unit="m"), +] +CourseCount: TypeAlias = Annotated[ + int, + symbol(glyph=r"n_{crs}", description="Number of shell courses", unit=""), +] +BoltsPerCourse: TypeAlias = Annotated[ + int, + symbol(glyph=r"n_{bolt,crs}", description="Bolts in one course", unit=""), +] +MaximumCourseHeight: TypeAlias = Annotated[ + float, + symbol( + glyph=r"h_{crs,max}", + description="Maximum height of one shell course", + unit="m", + ), +] +FillRatio: TypeAlias = Annotated[ + float, + symbol(glyph=r"r_{fill}", description="Liquid fill ratio", unit=""), +] +LiquidDensity: TypeAlias = Annotated[ + float, + symbol(glyph=r"\rho_{liq}", description="Liquid density", unit="kg/m^3"), +] +PlanRadius: TypeAlias = Annotated[ + float, + symbol(glyph=r"r_{plan}", description="Plan distance from the origin", unit="m"), +] +BearingDegrees: TypeAlias = Annotated[ + float, + symbol( + glyph=r"\theta_{bear}", + description="Plan bearing from the east axis toward north", + unit="deg", + ), +] +BaselineDiameter: TypeAlias = Annotated[ + float, + symbol(glyph=r"D_{base}", description="Baseline tank diameter", unit="m"), +] +CandidateDiameter: TypeAlias = Annotated[ + float, + symbol(glyph=r"D_{cand}", description="Candidate tank diameter", unit="m"), +] diff --git a/examples/cylinder-tank/reference.json b/examples/cylinder-tank/reference.json new file mode 100644 index 0000000..f3d8765 --- /dev/null +++ b/examples/cylinder-tank/reference.json @@ -0,0 +1,967 @@ +{ + "referenceVersion": "1", + "cases": [ + { + "id": "cylinder-tank-defaults", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Default inside diameter 2 m, height 6 m, wall 8 mm, steel density 7850 kg/m^3, 4 courses, 12 bolts per course, fill ratio 0.5, liquid density 1000 kg/m^3, plan radius 5 m, and bearing 0 degrees. Radius is diameter/2. Area is pi*radius**2. Circumference is pi*diameter. Thickness in metres is thickness_mm/1000. Developed area is circumference*height. Shell volume is developed area*thickness. Shell mass is volume*density. Course height is height/course_count. Required courses are ceil(height/1.5). Bolt count is course_count*bolts_per_course. Shortfall is max(required-course_count, 0). Fill ratio 0.5 selects the partial branch, so liquid height is height*fill_ratio. Liquid volume is base area*liquid height and liquid mass is volume*density. Freeboard is max(height-liquid height, 0). Bearing radians are math.radians(bearing). East and north are radius*cos and radius*sin. Radial offset is hypot(east, north). Recovered bearing is degrees(atan2(north, east)).", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "estimate.cso.py", + "entrySourceHash": "76f8dfa0df37f739d05552bb4b72b85d5f36af69b39b0dbca5302b281c7c6b33", + "sourceClosureHash": "487dabf463d097fc954087ba32020a5590edd114013edc68e8d8ef9e1e001e49", + "function": "estimate", + "resolvedInputs": { + "diameter": 2.0, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12, + "fill_ratio": 0.5, + "liquid_density": 1000.0, + "plan_radius": 5.0, + "bearing_degrees": 0.0 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_height\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_volume\"]", + "value": 9.42477796076938, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_mass\"]", + "value": 9424.77796076938, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"freeboard\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"bearing_radians\"]", + "value": 0.0, + "unit": "rad" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"east_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"north_offset\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"radial_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"recovered_bearing\"]", + "value": 0.0, + "unit": "deg" + } + ] + }, + { + "id": "cylinder-tank-empty", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Same defaults except fill ratio 0. The comparison chain 0 < fill_ratio < 1 is false and fill_ratio >= 1 is false, so liquid height is the empty-branch literal 0. Liquid volume and mass are 0. Freeboard is the shell height. The unselected partial and full branches are not evaluated.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "estimate.cso.py", + "entrySourceHash": "76f8dfa0df37f739d05552bb4b72b85d5f36af69b39b0dbca5302b281c7c6b33", + "sourceClosureHash": "487dabf463d097fc954087ba32020a5590edd114013edc68e8d8ef9e1e001e49", + "function": "estimate", + "resolvedInputs": { + "diameter": 2.0, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12, + "fill_ratio": 0, + "liquid_density": 1000.0, + "plan_radius": 5.0, + "bearing_degrees": 0.0 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_height\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_volume\"]", + "value": 0.0, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_mass\"]", + "value": 0.0, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"freeboard\"]", + "value": 6.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"bearing_radians\"]", + "value": 0.0, + "unit": "rad" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"east_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"north_offset\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"radial_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"recovered_bearing\"]", + "value": 0.0, + "unit": "deg" + } + ] + }, + { + "id": "cylinder-tank-full", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Same defaults except fill ratio 1. The partial-fill chain is false and fill_ratio >= 1 is true, so liquid height is the shell height. Freeboard is 0. Liquid volume is the full inside cylinder.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "estimate.cso.py", + "entrySourceHash": "76f8dfa0df37f739d05552bb4b72b85d5f36af69b39b0dbca5302b281c7c6b33", + "sourceClosureHash": "487dabf463d097fc954087ba32020a5590edd114013edc68e8d8ef9e1e001e49", + "function": "estimate", + "resolvedInputs": { + "diameter": 2.0, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12, + "fill_ratio": 1, + "liquid_density": 1000.0, + "plan_radius": 5.0, + "bearing_degrees": 0.0 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_height\"]", + "value": 6.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_volume\"]", + "value": 18.84955592153876, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_mass\"]", + "value": 18849.55592153876, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"freeboard\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"bearing_radians\"]", + "value": 0.0, + "unit": "rad" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"east_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"north_offset\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"radial_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"recovered_bearing\"]", + "value": 0.0, + "unit": "deg" + } + ] + }, + { + "id": "cylinder-tank-bearing-90", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Same defaults except bearing 90 degrees. The radian conversion, cosine, sine, hypot, atan2, and degree conversion use Python math on that bearing. The other formulas keep the default inputs.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "estimate.cso.py", + "entrySourceHash": "76f8dfa0df37f739d05552bb4b72b85d5f36af69b39b0dbca5302b281c7c6b33", + "sourceClosureHash": "487dabf463d097fc954087ba32020a5590edd114013edc68e8d8ef9e1e001e49", + "function": "estimate", + "resolvedInputs": { + "diameter": 2.0, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12, + "fill_ratio": 0.5, + "liquid_density": 1000.0, + "plan_radius": 5.0, + "bearing_degrees": 90 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_height\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_volume\"]", + "value": 9.42477796076938, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_mass\"]", + "value": 9424.77796076938, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"freeboard\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"bearing_radians\"]", + "value": 1.5707963267948966, + "unit": "rad" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"east_offset\"]", + "value": 3.061616997868383e-16, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"north_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"radial_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"recovered_bearing\"]", + "value": 90.0, + "unit": "deg" + } + ] + }, + { + "id": "cylinder-tank-three-courses", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Same defaults except 3 shell courses. Required courses remain ceil(6/1.5)=4, so the shortfall is max(4-3, 0)=1. Bolt count is 3*12=36. Course height is 6/3=2. Plate area, volume, and mass do not use the course count.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "estimate.cso.py", + "entrySourceHash": "76f8dfa0df37f739d05552bb4b72b85d5f36af69b39b0dbca5302b281c7c6b33", + "sourceClosureHash": "487dabf463d097fc954087ba32020a5590edd114013edc68e8d8ef9e1e001e49", + "function": "estimate", + "resolvedInputs": { + "diameter": 2.0, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 3, + "bolts_per_course": 12, + "fill_ratio": 0.5, + "liquid_density": 1000.0, + "plan_radius": 5.0, + "bearing_degrees": 0.0 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities\",\"course_height\"]", + "value": 2.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"bolt_count\"]", + "value": 36, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/shell_quantities/course_bolts\",\"course_shortfall\"]", + "value": 1, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_height\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_volume\"]", + "value": 9.42477796076938, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"liquid_mass\"]", + "value": 9424.77796076938, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/liquid\",\"freeboard\"]", + "value": 3.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"bearing_radians\"]", + "value": 0.0, + "unit": "rad" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"east_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"north_offset\"]", + "value": 0.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"radial_offset\"]", + "value": 5.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/centre\",\"recovered_bearing\"]", + "value": 0.0, + "unit": "deg" + } + ] + }, + { + "id": "compare-shells-defaults", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Baseline diameter 2 m and candidate diameter 2.5 m, with shared height 6 m, wall 8 mm, density 7850 kg/m^3, 4 courses, and 12 bolts per course. Each shell repeats the plate and course formulas. The mass ratio is the candidate shell mass divided by the baseline shell mass. Mass is proportional to diameter, so the ratio is the diameter ratio when the binary64 division agrees.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "compare.cso.py", + "entrySourceHash": "2dd980a5020b27858fb0440191cae86ee3f41132bbb11fbd7174a1774ce49d9d", + "sourceClosureHash": "b2847df4c81df3eeb5e7c6b4298db9ab7d6041d2f852533c4ccae4d5b34c9f93", + "function": "compare_shells", + "resolvedInputs": { + "baseline_diameter": 2.0, + "candidate_diameter": 2.5, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root\",\"mass_ratio\"]", + "value": 1.25, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"developed_area\"]", + "value": 47.12388980384689, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"shell_volume\"]", + "value": 0.37699111843077515, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"shell_mass\"]", + "value": 2959.380279681585, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"radius\"]", + "value": 1.25, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"area\"]", + "value": 4.908738521234052, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"circumference\"]", + "value": 7.853981633974483, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + } + ] + }, + { + "id": "compare-shells-equal-diameter", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Both diameters are 2 m and the other comparison inputs keep their defaults. The two shell masses are the same binary64 quantity, so their ratio is 1.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "compare.cso.py", + "entrySourceHash": "2dd980a5020b27858fb0440191cae86ee3f41132bbb11fbd7174a1774ce49d9d", + "sourceClosureHash": "b2847df4c81df3eeb5e7c6b4298db9ab7d6041d2f852533c4ccae4d5b34c9f93", + "function": "compare_shells", + "resolvedInputs": { + "baseline_diameter": 2.0, + "candidate_diameter": 2, + "height": 6.0, + "thickness_mm": 8.0, + "density": 7850.0, + "course_count": 4, + "bolts_per_course": 12 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root\",\"mass_ratio\"]", + "value": 1.0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/baseline_shell/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"developed_area\"]", + "value": 37.69911184307752, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"shell_volume\"]", + "value": 0.30159289474462014, + "unit": "m^3" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"shell_mass\"]", + "value": 2367.504223745268, + "unit": "kg" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell\",\"course_height\"]", + "value": 1.5, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/base_circle\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/wall_thickness\",\"thickness_m\"]", + "value": 0.008, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"required_course_count\"]", + "value": 4, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"bolt_count\"]", + "value": 48, + "unit": "" + }, + { + "symbolId": "[\"symbol\",\"root/candidate_shell/course_bolts\",\"course_shortfall\"]", + "value": 0, + "unit": "" + } + ] + }, + { + "id": "published-circle-diameter-2", + "revision": "hand-derived-binary64", + "basis": { + "method": "Python math and arithmetic evaluated independently of cso execution", + "derivation": "Diameter 2 m. Radius is diameter/2, circumference is pi*diameter, and area is pi*radius**2. Glyphs d, r, C, and A are the notation of https://en.wikipedia.org/wiki/Circle.", + "sourceDescription": "Hand-derived expected values for the cylindrical tank sources. The numbers come from the stated formulas, not from a cso result. This binding does not assert human engineering approval of a generated document." + }, + "binding": { + "entryModuleId": "published_circle.cso.py", + "entrySourceHash": "195f7e0ec18283c1f68b0a77ba12bfca0a1719f9b3b5f7c1477b7408f568a36f", + "sourceClosureHash": "6218de27926e62d663bcbc5e6f66ba02a2e8b1fb7758bd35e43b98081db66add", + "function": "published_circle", + "resolvedInputs": { + "diameter": 2.0 + } + }, + "expected": [ + { + "symbolId": "[\"symbol\",\"root\",\"radius\"]", + "value": 1.0, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root\",\"circumference\"]", + "value": 6.283185307179586, + "unit": "m" + }, + { + "symbolId": "[\"symbol\",\"root\",\"area\"]", + "value": 3.141592653589793, + "unit": "m^2" + } + ] + } + ] +} diff --git a/examples/cylinder-tank/references/published_circle.cso.py b/examples/cylinder-tank/references/published_circle.cso.py new file mode 100644 index 0000000..6e11ca5 --- /dev/null +++ b/examples/cylinder-tank/references/published_circle.cso.py @@ -0,0 +1,39 @@ +"""Glyph-faithful transcription of the elementary circle relations.""" + +import math +from typing import Annotated + +from cso_python import CalculationResults, calculation, section, symbol, text + + +@calculation( + id="published-circle", + title="Published circle relations", + metadata={ + "referenceUrl": "https://en.wikipedia.org/wiki/Circle", + }, +) +@section(id="published-circle", title="Published circle relations", root=True) +def published_circle( + diameter: Annotated[ + float, + symbol(glyph="d", description="Diameter", unit="m"), + ] = 2.0, +) -> CalculationResults: + text( + id="source", + content="Reproduction of the elementary circle relations: circumference equals pi times diameter, and area equals pi times radius squared. Reference: https://en.wikipedia.org/wiki/Circle. The glyphs d, r, C, and A are that reference notation.", + ) + radius: Annotated[ + float, + symbol(glyph="r", description="Radius", unit="m"), + ] = diameter / 2 + circumference: Annotated[ + float, + symbol(glyph="C", description="Circumference", unit="m"), + ] = math.pi * diameter + area: Annotated[ + float, + symbol(glyph="A", description="Area", unit="m^2"), + ] = math.pi * radius**2 + return {"circumference": circumference, "area": area} diff --git a/examples/cylinder-tank/shell.cso.py b/examples/cylinder-tank/shell.cso.py new file mode 100644 index 0000000..58b1ad8 --- /dev/null +++ b/examples/cylinder-tank/shell.cso.py @@ -0,0 +1,80 @@ +"""Open cylindrical shell: developed wall, steel quantity, and course schedule.""" + +from typing import Annotated + +from _cso_bindings.courses import shell_courses +from _cso_bindings.geometry.circle import circle +from _cso_bindings.thickness import metres_from_millimetres +from cso_python import ( + CalculationResults, + calculation, + document_section, + section, + symbol, + text, +) +from quantities import ( + BoltsPerCourse, + CourseCount, + SteelDensity, + TankDiameter, + TankHeight, + WallThicknessMillimetres, +) + + +@calculation(id="tank-shell", title="Cylindrical shell") +@section(id="shell", title="Cylindrical shell") +def shell( + diameter: TankDiameter, + height: TankHeight, + thickness_mm: WallThicknessMillimetres = 8.0, + density: SteelDensity = 7850.0, + course_count: CourseCount = 4, + bolts_per_course: BoltsPerCourse = 12, +) -> CalculationResults: + base_circle = circle(diameter=diameter) + wall_thickness = metres_from_millimetres(thickness_mm=thickness_mm) + course_bolts = shell_courses( + course_count=course_count, + bolts_per_course=bolts_per_course, + height=height, + ) + + with document_section(id="shell-plate", title="Shell plate"): + text( + id="plate", + content="Open vertical cylinder. The developed wall excludes roof and floor plates. dev = developed plate; shl = shell.", + ) + developed_area: Annotated[ + float, + symbol( + glyph=r"A_{dev}", + description="Developed shell area", + unit="m^2", + ), + ] = base_circle["circumference"] * height + shell_volume: Annotated[ + float, + symbol(glyph=r"V_{shl}", description="Shell steel volume", unit="m^3"), + ] = developed_area * wall_thickness["thickness_m"] + shell_mass: Annotated[ + float, + symbol(glyph=r"m_{shl}", description="Shell steel mass", unit="kg"), + ] = shell_volume * density + + course_height: Annotated[ + float, + symbol(glyph=r"h_{crs}", description="Height of one shell course", unit="m"), + ] = height / course_count + + return { + "area": base_circle["area"], + "circumference": base_circle["circumference"], + "shell_volume": shell_volume, + "shell_mass": shell_mass, + "course_height": course_height, + "bolt_count": course_bolts["bolt_count"], + "required_course_count": course_bolts["required_course_count"], + "course_shortfall": course_bolts["course_shortfall"], + } diff --git a/examples/cylinder-tank/tank.svg b/examples/cylinder-tank/tank.svg new file mode 100644 index 0000000..2a93fa7 --- /dev/null +++ b/examples/cylinder-tank/tank.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + Dtank + htank + twall,mm + + diff --git a/examples/cylinder-tank/thickness.cso.py b/examples/cylinder-tank/thickness.cso.py new file mode 100644 index 0000000..0d72fce --- /dev/null +++ b/examples/cylinder-tank/thickness.cso.py @@ -0,0 +1,17 @@ +"""Convert shell thickness from millimetres to metres.""" + +from cso_python import CalculationResults, calculation, section, text +from quantities import WallThicknessMetres, WallThicknessMillimetres + + +@calculation(id="wall-thickness", title="Wall thickness conversion") +@section(id="thickness", title="Wall thickness") +def metres_from_millimetres( + thickness_mm: WallThicknessMillimetres, +) -> CalculationResults: + text( + id="conversion", + content="Wall thickness is supplied in millimetres and converted to metres before a metre formula uses it. The qualifier mm marks the millimetre input. There is no automatic unit conversion.", + ) + thickness_m: WallThicknessMetres = thickness_mm / 1000 + return {"thickness_m": thickness_m}