Skip to content

Commit 2c45ae6

Browse files
authored
Add docs for microwave cycle fitting (#18)
* Fix on docs what is already in README * Minor revision * Add a script, still needs docs comments * Put some better text in the example for documentation * Touch up docs elsewhere
1 parent 9b353d8 commit 2c45ae6

File tree

9 files changed

+6867
-25
lines changed

9 files changed

+6867
-25
lines changed

docs/example/2023-03-02_RF_Mannitol_process.csv

Lines changed: 1209 additions & 0 deletions
Large diffs are not rendered by default.

docs/example/2023-03-02_RF_Mannitol_temperature.csv

Lines changed: 5390 additions & 0 deletions
Large diffs are not rendered by default.

docs/example/2024-06-04-10_MFD_AH.csv

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
Run started: 6/4/2024 10:38:37 AM,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
2-
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
3-
Product Name: Man5 controlled anneal,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
4-
Product Number: 1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
5-
Operator: ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
6-
Recipe File Name: man5.rcp,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
7-
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1+
Run started: 6/4/2024 10:38:37 AM
2+
3+
Product Name: Man5 controlled anneal
4+
Product Number: 1
5+
Recipe File Name: man5.rcp
6+
87
CycleTime,Cycle,Phase,Step,VacSetPT,VacPirani,VacCPM,ShelfSetPT,ShelfInlet,Condenser,TPAvg,TP1,TP2,TP3,TP4,TP5,TP6,TP7,TP8,TShelfSurface,TProdConsider,HeatFlux,KvShelf,MassFlowVial,MassFlowHR,RProd,PercentDried,GroupAvg,Qshelf,KvSys_SS,KvFinal_SS,HFSystem,Percent_FRZ,LyoSIM,LyoSIM_SetPT,Percent_RM
98
10:38:38,9,1,0,500,738690,0,-25,2.6,9.6,3.4,5.2,3.2,999.9,1.9,999.9,999.9,999.9,999.9,0,5.2,-4004027,0,0,0,0,0,5.2,100,0,0,-4004027,NaN,1.3,-25,0
109
10:39:39,9,1,1,0,463110,0,13.1,1.3,9.9,3.2,5.1,2.9,999.9,1.6,999.9,999.9,999.9,999.9,-0.6,5.1,0,0,0,0,0,0,5.1,100,0,0,0,NaN,-2.4,3,0

docs/example/all_recipes.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ using Accessors
2525

2626
# # Read Process Data
2727

28+
# This needs to point to the right file, which for documentation is kinda wonky;
29+
# adjust this in to refer to an appropriate file.
2830
## Data start at 8th row of CSV file.
29-
## This needs to point to the right file, which for documentation is kinda wonky
30-
procdata_raw = CSV.read(joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv"), Table, header=8)
31+
file_loc = joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv")
32+
procdata_raw = CSV.read(file_loc, Table, header=7)
3133
t = uconvert.(u"hr", procdata_raw.CycleTime .- procdata_raw.CycleTime[1])
3234
## At midnight, timestamps revert to zero, so catch that case
3335
for i in eachindex(t)[begin+1:end]

docs/example/fitting_mannitol.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
# `using LyoPronto`, you have effectively also done `using Unitful` and a few others.
44
using LyoPronto
55

6-
# These are other packages that I use in the test suite,
6+
# These packages are used in the test suite,
77
# but you can use others in their place.
8-
# TypedTables provides a lightweight table structure, not as broadly flexible as a DataFrame but great for our needs
8+
9+
# TypedTables provides a lightweight table structure, not as broadly flexible as a DataFrame but great for our needs.
910
using TypedTables, CSV
1011
# TransformVariables provides tools for mapping optimization parameters to sensible ranges.
1112
using TransformVariables
@@ -25,7 +26,8 @@ using LaTeXStrings
2526

2627
## Data start at 8th row of CSV file.
2728
## This needs to point to the right file, which for documentation is kinda wonky
28-
procdata_raw = CSV.read(joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv"), Table, header=8)
29+
file_loc = joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv")
30+
procdata_raw = CSV.read(file_loc, Table, header=7)
2931
## MicroFD, used for this experiment, has a column indicating primary drying
3032
t = uconvert.(u"hr", procdata_raw.CycleTime .- procdata_raw.CycleTime[1])
3133
## At midnight, timestamps revert to zero, so catch that case
@@ -35,6 +37,12 @@ for i in eachindex(t)[begin+1:end]
3537
end
3638
end
3739

40+
cp(file_loc, "./2024-06-04-10_MFD_AH.csv"); #md #hide
41+
cp(file_loc[1:end-4] * ".yaml", "./2024-06-04-10_MFD_AH.yaml"); #md #hide
42+
# If you want to follow along exactly, you can download the CSV file
43+
# [here](2024-06-04-10_MFD_AH.csv), and some metadata about the cycle
44+
# [here](2024-06-04-10_MFD_AH.yaml).
45+
3846
## Rename the columns we will use, and add units
3947
procdata = map(procdata_raw) do row
4048
## In the anonymous `do` function, `row` is a row of the table.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# # Imports
2+
using LyoPronto
3+
4+
# NonlinearSolve and TransformVariables are used for parameter fitting
5+
using NonlinearSolve
6+
using TransformVariables
7+
8+
# CSV and TypedTables load and store experimental data
9+
using TypedTables, CSV
10+
11+
# Plots is a frontend to several plotting packages, defaulting to GR
12+
using Plots
13+
14+
## All we need from StatsPlots is the @df macro
15+
using StatsPlots: @df
16+
17+
## Latexify helps with LaTeX formatting in plots
18+
using Latexify
19+
using LaTeXStrings
20+
## Set a default for Latexify labels
21+
set_default(labelformat=:square) # Latexify, not Plots
22+
23+
24+
25+
# # Load process data
26+
# The process data here are from an actual microwave-assisted lyophilization cycle.
27+
# Since thermocouples cannot be used in a microwave field, the product temperature
28+
# measurements (from fiber optic probes) are in a separate file, and we must take care to
29+
# synchronize the time.
30+
31+
# As in the other examples, the file locations here are wonky in order to execute this
32+
# documentation remotely; to follow along, adjust the file paths to match your local setup.
33+
34+
filesdir = joinpath(@__DIR__, "..", "..", "example")
35+
36+
dat1 = CSV.read(joinpath(filesdir, "2023-03-02_RF_Mannitol_temperature.csv"), Table)
37+
dat2 = CSV.read(joinpath(filesdir, "2023-03-02_RF_Mannitol_process.csv"), Table,
38+
comment="#", stripwhitespace=true, dateformat="mm/dd/yyyy H:M:S")
39+
40+
cp(joinpath(filesdir, "2023-03-02_RF_Mannitol_temperature.csv"), "./2023-03-02_RF_Mannitol_temperature.csv"); #md #hide
41+
cp(joinpath(filesdir, "2023-03-02_RF_Mannitol_process.csv"), "./2023-03-02_RF_Mannitol_process.csv"); #md #hide
42+
# To follow along, you can use the same data files [here](2023-03-02_RF_Mannitol_process.csv)
43+
# and [here](2023-03-02_RF_Mannitol_temperature.csv).
44+
45+
time1 = dat1.var"Elapsed [s]"
46+
47+
lyo_full_pre = map(dat2) do row
48+
nt = (tstamp = row.Timestamp,
49+
pch_sp = row.var"SPLYO.VACUUM_SP.F_CV"*u"mTorr",
50+
pch_pir = row.var"SPLYO.CHAMBER_PIRANI.F_CV"*u"mTorr",
51+
pch_cm = row.var"SPLYO.CHAMBER_CM.F_CV"*u"mTorr",
52+
Tsh_sp = row.var"SPLYO.SHELF_SP.F_CV"*u"°C",
53+
Tsh_i = row.var"SPLYO.SHELF_INLET.F_CV"*u"°C",
54+
Tsh_o = row.var"SPLYO.SHELF_OUTLET.F_CV"*u"°C",
55+
)
56+
return nt
57+
end
58+
time2 = uconvert.(u"hr", lyo_full_pre.tstamp .- lyo_full_pre.tstamp[1])
59+
lyo_full = Table(lyo_full_pre; t=time2)
60+
61+
# Here we are concerned with the primary drying step, so we need to identify that.
62+
istart_lyo_pd = findfirst(lyo_full.pch_sp .== 100u"mTorr")
63+
lyo_pd = Table(lyo_full[istart_lyo_pd:end])
64+
## Set start of primary drying as zero time
65+
lyo_pd.t .-= lyo_pd.t[1]
66+
nothing #md #hide
67+
68+
# It's a good idea at this point to plot temperatures and pressures,
69+
# to make sure everything looks right.
70+
plT = plot(lyo_pd.t, lyo_pd.Tsh_sp)
71+
plp = plot(lyo_pd.t, lyo_pd.pch_pir, ylim=(0, 300))
72+
plot!(plp, lyo_pd.t, lyo_pd.pch_cm)
73+
plot(plT, plp, layout=(2,1), link=:x)
74+
75+
# We will use this to set the shelf temperature and pressure that are used later in fitting.
76+
Tsh = RampedVariable(uconvert.(u"K", [-40.0u"°C", 10.0u"°C"]), 0.5u"K/minute")
77+
pch = RampedVariable(100u"mTorr")
78+
79+
# We will also need to know when primary drying *actually* ends,
80+
# so we will use the second-derivative test in Pirani-CM convergence.
81+
t_end = identify_pd_end(lyo_pd.t, lyo_pd.pch_pir, Val(:der2))
82+
@df lyo_pd plot(:t, :pch_pir)
83+
@df plot!(:t, pch_pir_sm)
84+
tendplot!(t_end)
85+
86+
87+
# Next, we need to read the temperature data from the fiber optic probes.
88+
thm_full = map(dat1) do row
89+
nt = (tstamp = row.var"Elapsed [s]"*u"s",
90+
T1 = row.var"T1 [C]"*u"°C",
91+
T2 = row.var"T2 [C]"*u"°C",
92+
T3 = row.var"T3 [C]"*u"°C",
93+
T4 = row.var"T4 [C]"*u"°C",
94+
)
95+
return nt
96+
end
97+
@df thm_full exptfplot(:tstamp, :T1, :T2, :T3, :T4, nmarks=40, xunit=u"hr")
98+
99+
# Clearly we can ignore T2, which is not reading anything physical.
100+
# Also, note that one of our three remaining thermal probes is on the outside wall of a vial,
101+
# while the other two are in frozen product. Based on the behavior we see in plots, it is
102+
# clear that the series denoted T3 is the odd one out.
103+
104+
# Next, we need to identify the portion of the temperature data that corresponds to primary drying.
105+
# After some trial and error, I decided the following achieves that:
106+
istart_thm_pd = argmin(thm_full.T1)
107+
iend_thm_pd = argmax(thm_full.T4) + 600
108+
109+
thm_pd_sub = Table(thm_full[istart_thm_pd:iend_thm_pd])
110+
thm_pd = Table(thm_pd_sub; t = uconvert.(u"hr", thm_pd_sub.tstamp .- thm_pd_sub.tstamp[1]))
111+
112+
## Plot our temperatures to make sure everything looks right
113+
plot(u"hr", u"°C", xlabel="Time", ylabel="Temperature", size=(400,300), legend=false)
114+
@df thm_pd exptfplot!(:t, :T1, :T4, nmarks=40)
115+
@df thm_pd exptvwplot!(:t, :T3, nmarks=40)
116+
plot!(Tsh, c=:black, label="shelf", tmax=13.5u"hr")
117+
118+
# Based on that plot, we identify the part that we want to fit:
119+
# everything up until the temperature minimum near 10 hours.
120+
iend_T4 = argmin(thm_pd.T4[1000:end]) + 1000
121+
iend_T1 = argmin(thm_pd.T1[1000:end]) + 1000
122+
## Factor of 6 is because the temperature data are every 10 seconds,
123+
## compared to every minute for process data
124+
fitdat = @df thm_pd[begin:6:end] PrimaryDryFit(:t, (:T4[begin:iend_T4÷6],
125+
:T1[begin:iend_T1÷6]), :T3, t_end);
126+
plot(fitdat)
127+
128+
# # Set up other model parameters
129+
## Vial parameters
130+
vialsize = "6R"
131+
rad_i, rad_o = get_vial_radii(vialsize)
132+
A_p = π*rad_i^2 # cross-sectional area inside the vial
133+
A_v = π*rad_o^2 # vial bottom area
134+
m_v = get_vial_mass(vialsize)
135+
## Formulation and fill
136+
c_solid = 0.05u"g/mL" # g solute / mL solution
137+
ρ_solution = 1u"g/mL" # g/mL total solution density
138+
R0 = 1.4u"cm^2*hr*Torr/g"
139+
A1 = 16.0u"cm*hr*Torr/g"
140+
A2 = 0.0u"1/cm"
141+
Rp = RpFormFit(R0, A1, A2)
142+
Vfill = 5u"mL"
143+
## Heat transfer
144+
KC = 2.75e-4u"cal/s/K/cm^2"
145+
KP = 8.93e-4u"cal/s/K/cm^2/Torr"
146+
KD = 0.46u"1/Torr"
147+
K_shf_f = RpFormFit(KC, KP, KD)
148+
## Geometry
149+
h_f0 = Vfill/A_p
150+
m_f0 = Vfill * ρ_solution
151+
## RF fit parameters (dummy values, will be replaced in fitting)
152+
Bf = 2.0e7u"Ω/m^2"
153+
Bvw = 0.9e7u"Ω/m^2"
154+
Kvwf = 10.0u"W/K/m^2"
155+
## Controllable inputs
156+
f_RF = 8u"GHz"
157+
## Total of 10W nominal power, multiplied by 0.54 to account for system losses
158+
P_per_vial = RampedVariable(10u"W"/17 * 0.54) # actual power / vial
159+
160+
# We combine these into a `ParamObjRF` which will bundle up all the parameters we need.
161+
# This object needs to be constructed with all these parameters in this order,
162+
# so it has a constructor taking this tuple-of-tuples form that helps logical grouping.
163+
params_base = ParamObjRF((
164+
(Rp, h_f0, c_solid, ρ_solution),
165+
(K_shf_f, A_v, A_p),
166+
(pch, Tsh, P_per_vial),
167+
(m_f0, LyoPronto.cp_ice, m_v, LyoPronto.cp_gl),
168+
(f_RF, LyoPronto.eppf, LyoPronto.epp_gl),
169+
(Kvwf, Bf, Bvw),
170+
))
171+
# Estimates of some physical properties we will often need are included in LyoPronto,
172+
# including the heat capacity of ice and glass. A literature correlation for the dielectric
173+
# loss of ice is provided as `eppf`, and for glass we provide a more-uncertain
174+
# single value of `epp_gl`.
175+
176+
# # Parameter fitting
177+
# First, we set up the fit problem, and double check that our guess values are close-ish.
178+
## Transform variables to map from a 3-component vector to bounded physical values
179+
trans_KBB = KBB_transform_bounded(Kvwf, Bf, Bvw)
180+
## Create nonlinear least-squares function
181+
nls_M1 = NonlinearFunction{true}(nls_pd!, resid_prototype=zeros(num_errs(fitdat)))
182+
## Guess values for fit parameters, in log space
183+
## In practice, these often need tinkering with
184+
p0 = [3.0, 3.0, 0.3]
185+
## Check the physical solution for these guess values
186+
tsol = gen_sol_pd(p0, trans_KBB, params_base)
187+
modrftplot(tsol)
188+
plot!(fitdat, nmarks=40)
189+
190+
# Now, we actually run the fit, which is a nonlinear least squares problem
191+
192+
opt1 = solve(NonlinearLeastSquaresProblem(nls_M1, p0, (trans_KBB, params_base, fitdat)), LevenbergMarquardt())
193+
## Get the fitted solution
194+
prof_RF = gen_sol_pd(opt1.u, trans_KBB, params_base)
195+
## Get the fitted parameters in parameter space
196+
transform(trans_KBB, opt1.u)
197+
198+
# Now, we can plot the fit results against the experimental data.
199+
200+
## Set up the plot
201+
plT = plot(u"hr", u"°C", xlabel="Time", ylabel="Temperature")
202+
## Experimental data
203+
@df thm_pd exptfplot!(:t, :T4, :T1, nmarks=40)
204+
@df thm_pd exptvwplot!(:t, :T3, nmarks=40, label=L"$T_\mathrm{vw1}$, exp.")
205+
## Model results
206+
modrftplot!(prof_RF, markeralpha=0, trimend=1)
207+
## Shelf temperature
208+
plot!(Tsh, c=:black, label=L"T_\mathrm{sh}")
209+
## End of primary drying
210+
tendplot!(fitdat.t_end, label="", ls=:dash)
211+
## Mark the end of drying with a nice label
212+
annotate!(9, -32, Plots.text("end of drying,\nRF off", 12, "Computer Modern"))
213+
plot!([fitdat.t_end-1.5u"hr", fitdat.t_end-0.2u"hr"], [-30, -30], arrow=:arrow, c=:black, linewidth=1, label="")
214+
## Set other plot attributes
215+
plot!(legend=:topleft, ylim=(-40, 50), xlim=(0,13))
216+
plot!(size=(600,400), left_margin=15Plots.px)

docs/example/utilities.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ set_default(labelformat=:square) # Sets a Latexify default
2121

2222
## Data start at 8th row of CSV file.
2323
## This needs to point to the right file, which for documentation is kinda wonky
24-
procdata_raw = CSV.read(joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv"), Table, header=8)
24+
procdata_raw = CSV.read(joinpath(@__DIR__, "..", "..", "example", "2024-06-04-10_MFD_AH.csv"), Table, header=7)
2525
t = uconvert.(u"hr", procdata_raw.CycleTime .- procdata_raw.CycleTime[1])
2626
## At midnight, timestamps revert to zero, so catch that case
2727
for i in eachindex(t)[begin+1:end]

docs/make.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ using LyoPronto
33
using Documenter
44
using Literate
55

6-
@info "Using Literate to generate example"
7-
Literate.markdown((@__DIR__)*"/example/fitting_mannitol.jl", (@__DIR__)*"/src/generated", documenter=true)
8-
Literate.markdown((@__DIR__)*"/example/all_recipes.jl", (@__DIR__)*"/src/generated", documenter=true)
9-
Literate.markdown((@__DIR__)*"/example/utilities.jl", (@__DIR__)*"/src/generated", documenter=true)
6+
@info "Using Literate to generate examples"
7+
for file in ["fitting_mannitol.jl", "fitting_rf_mannitol.jl", "all_recipes.jl", "utilities.jl"]
8+
Literate.markdown((@__DIR__)*"/example/$file", (@__DIR__)*"/src/generated", documenter=true)
9+
end
1010

1111
@info "Building Documentation"
1212
makedocs(;
@@ -17,6 +17,7 @@ makedocs(;
1717
pages = [
1818
"Home" => "index.md",
1919
"Example, conventional lyo" => "generated/fitting_mannitol.md",
20+
"Example, microwave-assisted lyo" => "generated/fitting_rf_mannitol.md",
2021
"Other tools" => "generated/utilities.md",
2122
"Plot recipes" => "generated/all_recipes.md",
2223
"Reference" => "alldocstrings.md",

docs/src/index.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,44 @@ Some key advantages this has over the original version of LyoPRONTO are:
1919
## Installation
2020
As a Julia package, this code can be easily installed with the Julia package manager.
2121

22-
From the Julia REPL's Pkg mode (open a REPL and type `]` so that the prompt turns blue), add this package as a Git repo:
22+
You can add LyoPronto from the Julia General registry (so just like most other packages), using the Julia REPL's Pkg mode (open the REPL and type `]` so the prompt turns blue):
2323
```
24-
add https://github.com/LyoHUB/LyoPronto.jl.git
24+
add LyoPronto
2525
```
2626
`dev` can be substituted for `add` if you want to make changes to this package yourself, as explained in the [Julia Pkg manual](https://pkgdocs.julialang.org/v1/managing-packages/).
2727

2828
## Dependencies and Reexports
29-
This package leverages the strengths of the [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/) ecosystem to solve equations quickly and efficiently, although it only directly depends on `OrdinaryDiffEqRosenbrock` and `DiffEqCallbacks`, which are both reexported.
3029

31-
Also provided are plot recipes for [Plots.jl](https://docs.juliaplots.org/stable), although this package only depends on `RecipesBase`.
30+
The following are reexported by this package (so that you don't need to import them after importing LyoPronto):
31+
- [OrdinaryDiffEqRosenbrock](https://docs.sciml.ai/DiffEqDocs/stable/); used for solving the DAEs and ODEs inherent here
32+
- [DiffEqCallbacks](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/), used for ending DAE and ODE solves when drying ends
33+
- [Unitful](https://juliaphysics.github.io/Unitful.jl/stable/); specifically, the `u""` macro, `ustrip`, `uconvert`, and `NoUnits`, which is all the API surface needed for regular usage of LyoPronto.
34+
35+
Other noteworthy dependencies:
36+
- [TransformVariables.jl](https://tpapp.github.io/TransformVariables.jl/stable/), which is used to map vector spaces onto realistic parameter values for the inevitable parameter fitting step
37+
- LyoPronto provides [plot recipes](https://docs.juliaplots.org/stable/recipes/) for [Plots.jl](https://docs.juliaplots.org/stable), although it does not depend on Plots in full.
38+
39+
Plots.jl is used for plotting in this documentation, with the following defaults:
40+
```@example plot_defaults
41+
using Plots # hide
42+
default(:fontfamily, "Computer Modern")
43+
default(:framestyle, :box)
44+
default(:lw, 2)
45+
default(:markersize, 4)
46+
default(:markerstrokewidth, 0.5)
47+
default(:unitformat, :square)
48+
resetfontsizes(); scalefontsizes(1.2)
49+
```
3250

33-
Heavy use is made of [Unitful.jl](https://painterqubits.github.io/Unitful.jl/stable/), which is reexported.
3451

3552

3653
## Authors
3754

38-
Written by Isaac S. Wheeler, a PhD student at Purdue University.
55+
Written by Isaac S. Wheeler, a PhD student at Purdue University, advised by Vivek Narsimhan and Alina Alexeenko.
3956
This work was supported in part by funding for NIIMBL project PC4.1-307 .
4057

41-
## License
58+
## Licensing
4259

43-
None yet. My intentions are to use the MIT license once this has been published in a scientific journal.
60+
This package is released with the MIT license.
4461

4562

0 commit comments

Comments
 (0)