Notebook Interface for Distributing Mathematica Programs to End Users

Mike Grigola, CB&I

(RowDefault[, (Spacer1), , (Spacer1), ])  #WolframTechConf

About Us

Chicago Bridge and Iron (CB&I) is a commercial engineering, procurement and construction (EPC) company.

We use Mathematica internally to design our tanks. My team makes Mathematica tools, which are distributed to and used by engineers around the world.

Our software tools must be rigorously validated to ensure they adhere to building codes, but also flexible enough to allow engineers to perform supplemental calculations.

Thus, we developed NotebookInterface to offer the structure of a traditional program with the flexibility of a notebook.

Why Make a Notebook Interface Framework?

Our programs require many inputs from the user, more than can fit in a single Cell or Manipulate.

We could create a number of basic cells:

x="value";

But this is susceptible to user edits. Plus, we can’t easily provide guidance on what sort of value x should be, or provide immediate feedback when something is invalid.

There are also the FormObject functions, but we want things to be more like regular input code.

NotebookInterface can resolve these issues.

Notebook Interface

We use Interpretation to make a form-like interface that behaves like regular input a user would manually type. Evaluating a cell sets the values on the left-hand side, as usual.

Dynamic styling (the red frame) indicates required inputs and calls out errors

Users can also add their own code and notes as projects may require customization - something that wouldn't be possible with most software tools.

Input Aids

The unique parts of this interface are what we call “Input Aids”, the buttons on the right side of the notebook. The input aid opens a new cell on top of the notebook (FrontEnd`AttachCell). It provides a graphical interface to define inputs.

Input aids offer freedom to perform Dynamic updates and calculations that would otherwise clutter the notebook. 

Here we have a database of building materials that can be used to populate the required property inputs. Selecting a material sets the properties and calculates the allowed stress in this plate.

Pressing the Update Cell button pulls the content from the input aid back into the notebook

Calling Out and Tracking Changes

Changes made directly in the notebook differing from what was entered in the input aid are highlighted with a yellow background.

This feature can show edits in the latest revision, or allow users to explore a change without losing previous values.

The input aid stores the last values entered into the aid, so opening the input aid and accepting restores the original values to the notebook.

Managing Updates

When an input is deleted by the user or added in a newer version by the developer, 
the input aid prompts to replace the old cell with the new/complete content

A warning shows which expected inputs are missing

Clicking Replace Cell yields the current full set of inputs

This helps with upgrades, ensuring users have the complete inputs required 
for the latest version of the calculations

Functions and Intermediates

Functions and intermediate calculations can exist alongside input fields

The roofDimensions result holds additional dimensions the user may explore or incorporate in their own functions.

This line of code will update via the input aid if we make a change to the cell, the same as the input lines

More Complex Input

Most of our projects require large inputs that are difficult to type directly and must be permanently stored. The input aid is a great tool to deal with this.

This cell collects a matrix of factors you wouldn’t want to type directly into the notebook

The aid stores the data compactly in the notebook where it’s added to an Association with various other information about the tank.

Instructions and Help Text

Additional information about the inputs can be accessed in the input aid.
The information is hidden by default and expanded with the information icon

These hints can help newer or infrequent users work with the aid.

Supporting information can also be given directly in the notebook as Text cells or via the built-in documentation. We use all these methods extensively.

A Complete Notebook

The notebook may have dozens of separate cells to collect all the inputs. Together they constitute what we call a program. We use an input aid for every cell containing inputs.

Once all inputs are defined, the user can run the structural analysis functions we’ve developed.
Alternatively, the user can perform custom analysis with our inputs, or build upon the standard results. 

The inputs and outputs are accessible and editable by the user.

Example Input Cell Code

This code creates the plate material properties example from earlier (slightly simplified)

ResourceFunction["GitHubInstall"]["CBI-Storage-Solutions","NotebookInterface"]

Needs["NotebookInterface`"];
(*
define the cell template (what appears in the notebook)

when developing inside a package, you must use this With wrapper and MakeBoxes
for variable names to appear in the notebook exactly as typed (and not prefixed with your package name)
*)
With[{
 plateMaterialBox = MakeBoxes@materials["plate"]["material"]
,plateMaterialTypeBox = MakeBoxes@materials["plate"]["materialType"]
,plateElasticModulusBox = MakeBoxes@materials["plate"]["elasticModulus"]
,platePoissonsRatioBox = MakeBoxes@materials["plate"]["poissonsRatio"]
,plateYieldStrengthBox = MakeBoxes@materials["plate"]["yieldStrength"]
,plateUltimateStrengthBox = MakeBoxes@materials["plate"]["ultimateStrength"]
,plateStructuralQualityFactorBox = MakeBoxes@materials["plate"]["structuralQualityFactor"]
,plateDensityBox = MakeBoxes@materials["plate"]["density"]
,plateDensityExprBox = RowBox[{"1.05", "*", TagBox[1, "control"], TagBox[2, "control"]}]
},
materialPropertiesPlateCellTemplate[] := materialPropertiesPlateCellTemplate@@$$materialPropertiesPlateDflts;
materialPropertiesPlateCellTemplate[
 plateMaterial_
,plateMaterialType_
,plateElasticModulus_, plateElasticModulusUnit_
,platePoissonsRatio_
,plateYieldStrength_, plateYieldStrengthUnit_
,plateUltimateStrength_, plateUltimateStrengthUnit_
,plateStructuralQualityFactor_
,plateDensity_, plateDensityUnit_
] := {
 setRowTemplateBoxes[
plateMaterialBox
,{
inputFieldTemplate[
plateMaterial
,String
,(StringQ@# || #===Null)&
,""
,"plate material"
]
}
]
,setRowTemplateBoxes[
plateMaterialTypeBox
,{
popupMenuTemplate[
plateMaterialType
,{"Carbon Steel", "Stainless Steel", "Aluminum"}
]
}
]
,setRowTemplateBoxes[
plateElasticModulusBox
,{
inputFieldTemplate[
plateElasticModulus
,Expression
,(NumericQ@# && Positive@#)&
,"must be a positive number"
,"E_p"
,FieldSize->9
]
,unitsMenuTemplate[plateElasticModulusUnit, "Stress"]
}
]
,setRowTemplateBoxes[
platePoissonsRatioBox
,{
inputFieldTemplate[
platePoissonsRatio
,Expression
,(NumericQ@# && 0.0 < # < 1.0)&
,"must must be a number between 0 and 1"
,"ν_p"
,FieldSize->9
]
}
]
,setRowTemplateBoxes[
plateYieldStrengthBox
,{
inputFieldTemplate[
plateYieldStrength
,Expression
,(NumericQ@# && Positive@#)&
,"must be a positive number"
,"Fy_p"
,FieldSize->9
]
,unitsMenuTemplate[plateYieldStrengthUnit, "Stress"]
}
]
,setRowTemplateBoxes[
plateUltimateStrengthBox
,{
inputFieldTemplate[
plateUltimateStrength
,Expression
,(NumericQ@# && Positive@#)&
,"must be a positive number"
,"E_p"
,FieldSize->9
]
,unitsMenuTemplate[plateUltimateStrengthUnit, "Stress"]
}
]
,setRowTemplateBoxes[
plateStructuralQualityFactorBox
,{
inputFieldTemplate[
plateStructuralQualityFactor
,Expression
,(NumericQ@# && Positive@#)&
,"must be a positive number"
,"Q_p"
,FieldSize->6
]
}
]
(* example of adding a mixture of code and InputField on the same line*)
,verbatimSetRowTemplateBoxes[
{plateDensityBox, plateDensityExprBox}
,{
inputFieldTemplate[
plateDensity
,Expression
,(NumericQ@# && Positive@#)&
,"must be a positive number"
,"ρ_p"
,FieldSize->9
]
,unitsMenuTemplate[plateDensityUnit, "Density"]
}
]
}];



$stressUnit="PoundsForce"/"Inches"^2;
$densityUnit="Pounds"/"Inches"^3;

$$materialPropertiesPlateDflts := <|
 "plateMaterial" -> Null
,"plateMaterialType" -> "Carbon Steel"
,"plateElasticModulus" -> Null
,"plateElasticModulusUnit" -> Quantity[1, $stressUnit]
,"platePoissonsRatio" -> Null
,"plateYieldStrength" -> Null
,"plateYieldStrengthUnit" -> Quantity[1, $stressUnit]
,"plateUltimateStrength" -> Null
,"plateUltimateStrengthUnit" -> Quantity[1, $stressUnit]
,"plateStructuralQualityFactor" -> Null
,"plateDensity" -> Null
,"plateDensityUnit" -> Quantity[1, $densityUnit]
|>;


(* $$aidContext should be set to the string context name of the package containing the function e.g. "MyPackage`Inputs`". 
Here in this notebook it's just "Global`"
*)
$$aidContext="Global`";

(* inputAidFromTemplate creates a generic input aid with no custom content.
This behaves the same as the notebook/cell template, but with the benefits of an input aid: remembering previous values, managing updates, etc.
Custom code can be lengthy, e.g. see below
*)
(*materialPropertiesPlateInputAid[cellobj_CellObject]:=
With[{
aidName=$$aidContext<>"materialPropertiesPlateInputAid",
cellTemplate=$$aidContext<>"materialPropertiesPlateCellTemplate",
symbols=Keys@$$materialPropertiesPlateDflts
},
inputAidFromTemplate[cellobj,cellTemplate,symbols,aidName]
];*)
materialPropertiesPlateInputAid[cellobj_CellObject] :=
With[{
aidName = $$aidContext<>"materialPropertiesPlateInputAid"
,tags = Keys@$$materialPropertiesPlateDflts
},
(* note: this shows how to use an association to store vars instead of a list of symbols (+ handle cellTemplate vars != inputAidTemplate vars) *)
DynamicModule[{cell,validmsg,isvalid,isvalidInput,validmsgInput,vars=<||>},
With[{rawTaggingRules = Association@CurrentValue[cellobj, {TaggingRules, aidName}]},
vars = AssociationMap[Lookup[rawTaggingRules,#,Null]/.Inherited->Null &, tags];
];

{cell, validmsg, isvalid} = readCellAndValidateAgainstTemplate[cellobj, Global`materialPropertiesPlateCellTemplate[] ];
{isvalidInput, validmsgInput} = materialPropertiesPlateInputValidation[];

inputAidInterface[
cellobj
,materialPropertiesPlateInputAidInterface[isvalidInput, validmsgInput, vars, "USC"] (* only US units in limited example*)
,inputAidCellButtonAction[cellobj
,inputAidTemplateToBoxes[materialPropertiesPlateCellTemplate@@Map[vars, tags], aidName]
,aidName
,writeInputAidTaggingRules[tags, Values@vars]
] (* only $$Symbols go to cellTemplate, all Values go to TaggingRules *)
,inputAidCellButtonAction[cellobj
,First@replaceEvaporativeControlsInCellByTemplate[cell,materialPropertiesPlateCellTemplate@@Map[vars, tags] ]
,aidName
,writeInputAidTaggingRules[tags, Values@vars]
]
,isvalid
,validmsg
,"CellFormatWarningMessage" -> Automatic
,"HelpMessage" -> $$aidContext<>"materialPropertiesPlateHelp[]"
]
]
];

materialPropertiesPlateHelp[] :=
Column[{
 "Select a material designation to pull in its properties"
}, Spacings->{1,1.6}
];

materialPropertiesPlateInputValidation[] :=
Module[{isvalidInput=True, validmsgInput={}},
{isvalidInput, validmsgInput}
];


SetAttributes[uiQuantityRow,HoldFirst];
uiQuantityRow[vars_,cellTemplate_,rowNo_,key_]:=With[{unitKey=key<>"Unit",field=cellTemplate[[rowNo,"controls",1]],popup=cellTemplate[[rowNo,"controls",2]]},{RawBoxes@cellTemplate[[rowNo,"variable name"]]," = ",inputAidInputField[vars[key],field["input type"],"InputFieldValidationFunction"->field["validation function"],"InputFieldValidationMessage"->field["validation message"],FieldHint->field["field hint"],Background->White,Sequence@@field["InputField options"]],inputAidUnitsMenu[vars[unitKey],popup["unit type"],Sequence@@popup["PopupMenu options"]]}];

SetAttributes[uiPopupRow,HoldFirst];
uiPopupRow[vars_,cellTemplate_,rowNo_,key_]:=With[{popup=cellTemplate[[rowNo,"controls",1]]},{RawBoxes@cellTemplate[[rowNo,"variable name"]]," = ",inputAidPopupMenu[vars[key],popup["menu list"],Sequence@@popup["PopupMenu options"]],SpanFromLeft}];

SetAttributes[uiScalarRow,HoldFirst];
uiScalarRow[vars_,cellTemplate_,rowNo_,key_]:=With[{field=cellTemplate[[rowNo,"controls",1]]},{RawBoxes@cellTemplate[[rowNo,"variable name"]]," = ",inputAidInputField[vars[key],field["input type"],"InputFieldValidationFunction"->field["validation function"],"InputFieldValidationMessage"->field["validation message"],FieldHint->field["field hint"],Background->White,Sequence@@field["InputField options"]]}];

(*if invalid/missing quantity, nothing happens*)
SetAttributes[setQuantity,HoldRest];
setQuantity[Quantity[mag_,unit_],a_,b_]:=(a=mag;b=Quantity[1,unit];);

$$plateMaterialProperties=<|
"USC"-><|
"A36"-><|"materialType"->"Carbon Steel","yieldStrength"->Quantity[36000,"PoundsForce"/"Inches"^2],"ultimateStrength"->Quantity[58000,"PoundsForce"/"Inches"^2],"elasticModulus"->Quantity[29000000,"PoundsForce"/"Inches"^2],"poissonsRatio"->0.3,"density"->Quantity[0.284,"Pounds"/"Inches"^3],"structuralQualityFactor"->0.92|>,"A516-70"-><|"materialType"->"Carbon Steel","yieldStrength"->Quantity[38000,"PoundsForce"/"Inches"^2],"ultimateStrength"->Quantity[70000,"PoundsForce"/"Inches"^2],"elasticModulus"->Quantity[29000000,"PoundsForce"/"Inches"^2],"poissonsRatio"->0.3,"density"->Quantity[0.284,"Pounds"/"Inches"^3],"structuralQualityFactor"->1.0|>,"A240 Type 304"-><|"materialType"->"Stainless Steel","yieldStrength"->Quantity[30000,"PoundsForce"/"Inches"^2],"ultimateStrength"->Quantity[75000,"PoundsForce"/"Inches"^2],"elasticModulus"->Quantity[28920000,"PoundsForce"/"Inches"^2],"poissonsRatio"->0.291,"density"->Quantity[0.29,"Pounds"/"Inches"^3],"structuralQualityFactor"->1.0|>
|>
|>;

SetAttributes[materialPropertiesPlateInputAidInterface, HoldRest];
materialPropertiesPlateInputAidInterface[isvalidInput:True, validmsgInput_, vars_, unitSys_] :=
With[{cellTemplate = Global`materialPropertiesPlateCellTemplate[]},
Column[{
Grid[{
{Row[{
Style["Plate Material:  ", Bold]
,PopupMenu[
Dynamic[vars["plateMaterial"]
, (
vars["plateMaterial"] = #;
If[#=!="Custom",
vars["plateMaterialType"] = $$plateMaterialProperties[unitSys,#,"materialType"];
setQuantity[ $$plateMaterialProperties[unitSys,#,"elasticModulus"], vars["plateElasticModulus"], vars["plateElasticModulusUnit"]];
vars["platePoissonsRatio"] =  $$plateMaterialProperties[unitSys,#,"poissonsRatio"];
setQuantity[$$plateMaterialProperties[unitSys,#,"yieldStrength"], vars["plateYieldStrength"], vars["plateYieldStrengthUnit"]];
setQuantity[ $$plateMaterialProperties[unitSys,#,"ultimateStrength"], vars["plateUltimateStrength"], vars["plateUltimateStrengthUnit"]];
vars["plateStructuralQualityFactor"] =  $$plateMaterialProperties[unitSys,#,"structuralQualityFactor"];
setQuantity[ $$plateMaterialProperties[unitSys,#,"density"], vars["plateDensity"], vars["plateDensityUnit"]];
]
)&
]
, Keys@$$plateMaterialProperties[unitSys]
]
}]
,SpanFromLeft
}
,uiPopupRow[vars, cellTemplate, 2, "plateMaterialType"]
,uiQuantityRow[vars, cellTemplate, 3, "plateElasticModulus"]
,uiScalarRow[vars, cellTemplate, 4, "platePoissonsRatio"]
,uiQuantityRow[vars, cellTemplate, 5, "plateYieldStrength"]
,uiQuantityRow[vars, cellTemplate, 6, "plateUltimateStrength"]
,uiScalarRow[vars, cellTemplate, 7, "plateStructuralQualityFactor"]
,uiQuantityRow[vars, cellTemplate, 8, "plateDensity"]
}, Alignment->{{Right,Center,Left,Left}}
]
}, Alignment->Center, BaseStyle->{FontFamily->"Roboto"}, Dividers->{{},{3->Gray}}]
];

materialPropertiesPlateInputAidInterface[isvalidInput:False, validmsgInput_, ___] := inputAidInputErrorDisplay[validmsgInput];

nb=CreateNotebook[];

(* you may need to tweak options depending on the stylesheet you're using *)
CurrentValue[nb,{"PopupMenuBoxOptions",BaseStyle}]={FontSize->12};
CurrentValue[nb,{"PopupMenuBoxOptions",MenuStyle}]={FontSize->12};
CurrentValue[nb,{"ButtonBoxOptions",MenuStyle}]={FontSize->20};
CurrentValue[nb,Magnification]=.75;
(*CurrentValue[nb,PageWidth]=-9998;*)

With[{aidNamePlate=$$aidContext<>"materialPropertiesPlateInputAid"},
NotebookWrite[nb, 
inputAidTemplateToBoxes[
materialPropertiesPlateCellTemplate@@Values[$$materialPropertiesPlateDflts]
, aidNamePlate
, TaggingRules->{aidNamePlate->Normal[$$materialPropertiesPlateDflts]} 
,NumberMarks->False
] 
];
];

Try it Yourself

https://github.com/CBI-Storage-Solutions/NotebookInterface

We’ve uploaded the NotebookInterface package to github complete with examples and documentation.

It’s available for anyone to use and perhaps improve

How it Works: Low Level

We define a “cell template” that represents the input content of one cell. The cell template is used to transfer information back from the input aid and to identify user/version changes.

Ex, here’s the first row in the material properties example that holds the material name input field:

\[LeftAssociation]"row type"->"setRow","variable name"->RowBox[{RowBox[{"materials","[","\"plate\"","]"}],"[","\"material\"","]"}],"variable tooltip"->None,"controls"->{\[LeftAssociation]"control type"->"evaporativeInputField","control value"->Null,"input type"->String,"validation function"->(StringQ[#1]||#1===Null&),"validation message"->"","field hint"->"plate material","InputField options"->{}\[RightAssociation]},"compoundExpression"->True\[RightAssociation]

When writing the cell to a notebook, the cell template is converted to standard controls wrapped in Interpretation, which causes the controls to look like UI elements but evaluate to the displayed value.

InterpretationBox[
      TagBox[
       TooltipBox[
        FrameBox[
         InputFieldBox[Dynamic[NotebookInterface`Private`evaporativeInputFieldVariable$$], 
          String,
          Appearance->None,
          FieldHint->"plate material name",
          ImageSize->264],
         Background->Automatic,
         BaselinePosition->Baseline,
         FrameMargins->1,
         FrameStyle->Dynamic[
           If[(Or[
              StringQ[#], # === Null]& )[
             NotebookInterface`Private`evaporativeInputFieldVariable$$] === True, 
            GrayLevel[0.7], 
            Directive[Red, Thick]]],
         StripOnInput->False],
        DynamicBox[
         ToBoxes[
          If[(Or[
             StringQ[#], # === Null]& )[
            NotebookInterface`Private`evaporativeInputFieldVariable$$] === True, "", ""], 
          StandardForm]],
        TooltipStyle->Dynamic[
          If[(Or[
             StringQ[#], # === Null]& )[
            NotebookInterface`Private`evaporativeInputFieldVariable$$] === True, {
           Background -> Directive[{White, 
               Opacity[0]}], CellFrame -> None}, Automatic]]],
       Annotation[#, 
        Dynamic[
         If[(Or[
            StringQ[#], # === Null]& )[
           NotebookInterface`Private`evaporativeInputFieldVariable$$] === True, "", ""]], 
        "Tooltip"]& ],
      NotebookInterface`Private`evaporativeInputFieldVariable$$]

When clicking Update Cell in an input aid, we perform replacement using pattern matching on the box form of the cell expression, inserting the values from the input aid. Lines of code that aren’t part of the cell template aren’t changed.

DynamicModuleBox[{NotebookInterface`Private`evaporativeInputFieldVariable$$ = Null, 
     NotebookInterface`Private`orig$$ = Null}, 
     InterpretationBox[...
 
 ->
 
     DynamicModuleBox[{NotebookInterface`Private`evaporativeInputFieldVariable$$ = "A36", 
     NotebookInterface`Private`orig$$ = "A36"}, 
     InterpretationBox[...

If the cell is missing parts, then the cell is regenerated from the template before replacement.


Cell TaggingRules store the values for the input aid, so it remembers the previous value

TaggingRules->{
   "MyPackage`InputAids`Private`materialPropertiesPlateInputAid" -> {
    "plateMaterial" -> "A36", "plateMaterialType" -> "CS", "plateElasticModulus" -> 29000000, "plateElasticModulusUnit" -> 
     Quantity[1, "Inches"^(-2) "PoundsForce"], "platePoissonsRatio" -> 0.3, "plateYieldStrength" -> 36000, 
     "plateYieldStrengthUnit" -> Quantity[1, "Inches"^(-2) "PoundsForce"], "plateUltimateStrength" -> 58000, 
     "plateUltimateStrengthUnit" -> Quantity[1, "Inches"^(-2) "PoundsForce"], "plateStructuralQualityFactor" -> 0.92, 
     "plateDensity" -> 0.284, "plateDensityUnit" -> Quantity[1, "Inches"^(-3) "Pounds"]}}

There may be more tagging rules than there are inputs in the cell template. This allows storing values only used by the input aid but not needed for later calculations in the notebook.

Thanks to John Fultz and Lou D’Andria for the original concept that CB&I used to develop the NotebookInterface package