Getting Started

Intoduction to Cedar Waves

Cedar Waves is a high performance tool for post-processing continuous time data like from analog circuit simulators. The simulation data is usually piece-wise-linear from a simulation with non-uniform sampling.

High Level Features

  • Easy to use
  • High performance: process Gigabytes of data in milliseconds.
  • High capacity: algorithms are very memory efficient allowing users to process very large wavefiles.
  • Supports various signal types:
    • Continuous signals:
      • Various interpolation methods between samples: constant, piecewise-linear, Akima, quadratic splines, or qubic splines.
      • Continuous functions like sin
    • Discrete signals: no interpolation between data points.
    • Uniform and non-uniform sampled signals
    • Finite and infinite domains
    • Periodic and ZeroPad signals
  • Fast and flexible clipping of the signal's domain to zoom-in and zoom-out along x-axis.
  • Accurate: math performed on continuous signals is also performed between sample points (not just at the data points)
  • Easy to extend: add custom functions that run at full speed to build automated flows/measurements.

License

(C) Julia Computing 2022. All rights reserved.
A contract must be obtained through Julia Computing to use this software.

Installing Julia

Julia 1.6.x or higher is required. If not already installed it can be obtained from https://julialang.org/downloads.

Starting Julia

Once Julia is installed Julia can be installed from a terminal or a launching icon on Windows. See more details in the Julia manual here.

Installing Cedar Waves

A typical install involves starting julia and then adding the CedarWaves package from the built-in Julia package mananger:

Note

Installation details may vary depending on how IT has configured the packages repository.

$ julia
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.3 (2021-09-23)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> Pkg.add("CedarWaves")

Now the Cedar Waves is installed and the add command doesn't need to be run again.

Interactive Interpreter

The most basic way to use Cedar Waves is from the interactive interpreter (REPL). Typically users will experiment with the interactive interpreter and keep changes in a text file that runs the complete script. The REPL is a good place to experiment for new users.

To use the software start the julia REPL and then in the run using CedarWaves to load the package:

julia> using CedarWaves

Now all the functions are imported for use.

Creating a Signal

The following examples use a very basic signal to showcase a few features.

A common signal type is a continuous sampled signal with piecewise-linear (PWL) interpolation between samples. Let's create two vectors with the x and y values and make a two point wave:

julia> using CedarWaves # once at top of session/file
julia> xs = [0, 1]2-element Vector{Int64}: 0 1
julia> ys = [-1, 1]2-element Vector{Int64}: -1 1
julia> s = PWL(xs, ys) ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Signal with domain of [0.0 .. 1.0]:⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ┌────────────────────────────────────────────────────────┐ 1 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠤⠖⠚⠉ signal ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠴⠒⠉⠁⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠤⠒⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠖⠚⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠴⠒⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⣉⣩⠭⠟⠛⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠴⠒⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠤⠒⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢀⣀⠤⠖⠚⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -1 ⣀⡤⠴⠒⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ └────────────────────────────────────────────────────────┘0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀1

The REPL will quickly print out a low-resolution ASCII plot to provide instant visual feedback of the signal. The ASCII plots are rumored to use technology from 1970s phosphorous display oscilloscopes so don't feel bad if you want to bump it to get it to work. High resolution plots are also available.

Continuing on, let's take the absolute value of the signal:

julia> s2 = abs(s)     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Signal with domain of [0.0 .. 1.0]:⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
     ┌────────────────────────────────────────────────────────┐
   1 ⠙⠦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠴⠋ signal
     ⠀⠀⠈⠑⢦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠊⠁⠀⠀ 
     ⠀⠀⠀⠀⠀⠈⠓⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠚⠁⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠉⠳⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠲⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠖⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⢦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠳⢄⡀⠀⠀⠀⠀⢀⡠⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
   0 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠲⣄⣠⠖⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     └────────────────────────────────────────────────────────┘0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀1

While it looks quite simple the correct behavior of interpolating between points is overlooked in most other tools.

Signal Values

Let's verify the abs(s) by checking the y-values at a few different x-values. Signals act like functions so just like y = f(x), the signal is the f and pass it an x value to get the corresponding y value:

julia> s2(0)
1.0

julia> s2(0.25)
0.5

julia> s2(0.5)
0.0

julia> s2(1)
1.0

It looks correct.

Custom Measurements

Often users will create a common sequence of steps that they would like to re-use over and over. Users can easily add new functions that operate on signals.

There are many built-in functions but lets see if we can re-create the provided rms function.

The rms value is calculated with the following steps:

First square the signal:

julia> sq = s^2     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Signal with domain of [0.0 .. 1.0]:⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
     ┌────────────────────────────────────────────────────────┐
   1 ⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞ signal
     ⠙⢦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡴⠋ 
     ⠀⠀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⠁⠀⠀ 
     ⠀⠀⠀⠀⠈⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠁⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⠦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
   0 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠓⠲⠤⢤⣀⣀⣀⣀⣀⣀⣀⣀⡤⠤⠖⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
     └────────────────────────────────────────────────────────┘0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀1

Then integrate it:

julia> s3 = integral(s^2)0.3333333333333333

Divide by the duration:

julia> s4 = integral(s^2)/xspan(s)0.3333333333333333

And take the square root:

julia> rmsval = sqrt(integral(s^2)/xspan(s))0.5773502691896257

The initial signal s is equivalent to a triangular wave with amplitude A=1 and the analytical rms value of a triangle wave is A/sqrt(3). Lets check:

julia> theoretical = 1/sqrt(3)0.5773502691896258

It looks really close. Julia has an "approximately equal to" operator (or isapprox(a, b) function). In the REPL can by typed with \approx<tab>.

julia> theoretical ≈ rmsval
true

It agrees!

Now to make the rms re-usable we will create a new function named myrms like so:

julia> myrms(a_signal) = sqrt(integral(a_signal^2)/xspan(a_signal))myrms (generic function with 1 method)

Note the familiar math-like syntax to define a one-line function. (Julia also supports multi-line function definitions.)

Let's check it:

julia> myrms(s)0.5773502691896257

And we get the same answer but now we can re-use myrms instead of typing out the steps each time.

High Performance

To demonstrate performance let's create 1GB of data to do some operations on:

Warning

If 1GB is too much data for your computer reduce the size appropriately.

Note: Use div(a, b) or a \div<tab> b (÷) for integer division

julia> mem = 10^9 # 1 GB1000000000
julia> mem_num = sizeof(1.0) # 8 bytes per number (64 bits)8
julia> N = mem ÷ mem_num125000000

So 125 million points is needed for 1 GB of data. Let's create the x-axis values:

julia> @time t = range(0, 1, length = N+1)  # time points  0.005206 seconds (7.55 k allocations: 453.890 KiB, 99.66% compilation time)
0.0:8.0e-9:1.0
Note

When commands start with @ that means a macro is running which can read in the rest of the line and insert extra statements like to measure the time it takes for the function to run.

This returns very quickly and just contains the start, stop and step size. One extra point was added since there is both a start and end point so now the step size is a nice number.

Now to create the corresponding y-values let's create a modulated sinusoidal signal:

julia> fc = 1000 # 1000 Hz carrier1000
julia> f = 2 # 2 Hz signal2
julia> @time y = @. sin(2pi*fc*t)*cos(2pi*f*t) 2.397053 seconds (384.67 k allocations: 975.025 MiB, 1.75% gc time, 3.57% compilation time) 125000001-element Vector{Float64}: 0.0 5.0265482436269475e-5 0.00010053096474553573 0.00015079644680079552 0.0002010619284750456 0.00025132740964128273 0.00030159289017250374 0.00035185836994170534 0.00040212384882188436 0.00045238932668603747 ⋮ -0.00040212384900835364 -0.00035185837052628064 -0.0003015928902456904 -0.0002513274101125754 -0.00020106192934444425 -0.00015079644715880548 -0.0001005309655016517 -5.026548268099676e-5 -6.428332918551267e-13

Note it takes 2 or 3 seconds (depending on the computer) to generate the y-values and takes 1GB of memory.

Now create a piecewise-linear signal with the values:

julia> @time modulated = PWL(t, y)  0.052496 seconds (8 allocations: 752 bytes)
             ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Signal with domain of [0.0 .. 1.0]:⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
             ┌────────────────────────────────────────────────────────┐
    0.999995 ⣿⣶⣄⠀⠀⠀⠀⠀⠀⠀⠀⢠⣼⣿⣿⣶⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣶⡆⡀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣦⣿ signal
             ⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⣀⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⢀⣸⣿⣿⣿⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿ 
             ⣿⣿⣿⣿⣧⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⢀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⢸⣿⣿⣿⣿ 
             ⣿⣿⣿⣿⣿⣷⡀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⣼⣿⣿⣿⣿⣿ 
             ⣿⣿⣿⣿⣿⣿⣧⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣼⣿⣿⣿⣿⣿⣿ 
             ⣿⣿⣿⣿⣿⣿⠿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢻⣿⣿⣿⣿⣿⣿ 
             ⣿⣿⣿⣿⣿⡿⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⢻⣿⣿⣿⣿⣿ 
             ⣿⣿⣿⣿⡿⠀⠀⠀⠈⠿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⠀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⠙⠀⠀⠀⠀⠻⣿⣿⣿⣿ 
             ⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠙⣿⣿⣿⣿⣿⢿⡟⠁⠀⠀⠀⠀⠈⢸⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠻⣿⣿⣿ 
   -0.999995 ⢿⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠘⠿⣿⢹⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠘⠟⣿⣿⠻⠈⠀⠀⠀⠀⠀⠀⠀⠀⠁⢿⢿⣿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⡿ 
             └────────────────────────────────────────────────────────┘0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀1

Notice that the plot happens almost instantly even though there are 125 million points. Let's zoom in to see a few cycles of the carrier:

julia> clip(modulated, 0 .. 4/fc) # ".." means interval             ⠀⠀⠀⠀Clipped signal with parent domain of [0.0 .. 1.0]:⠀⠀⠀⠀
             ┌────────────────────────────────────────────────────────┐
    0.999995 ⠀⠀⡜⠉⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠉⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠉⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠉⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀ signal
             ⣸⠁⠈⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠈⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠈⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠈⣇⠀⠀⠀⠀⠀⠀⠀⠀ 
             ⢀⡇⠀⠀⠀⢸⡀⠀⠀⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⢸⡀⠀⠀⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⢸⡀⠀⠀⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⢸⡀⠀⠀⠀⠀⠀⠀⠀ 
             ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
             ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
             ⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉ 
             ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 
             ⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⢸⠁ 
             ⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⢀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⢀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⢀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⢀⡏ 
   -0.999956 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⣀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⣀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⣀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⣀⡜⠀⠀ 
             └────────────────────────────────────────────────────────┘0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀0.004

Now use the custom myrms function created above to calculate the rms value of the signal. This is a complex function that takes the continuous integral of the squared signal (see Custom Measurements):

julia> @time myrms(modulated)  0.327329 seconds (664.67 k allocations: 35.196 MiB, 96.32% compilation time)
0.49999999989470767

It was fast but the first time Julia runs code it compiles it and that took about 98% of the time. So let's run it again to get the true speed:

julia> @time myrms(modulated)  0.008802 seconds (8 allocations: 214.328 KiB)
0.49999999989470767

Don't blink, because you may miss it. But is it correct?

According to Wolfram Alpha the integral of the y-values squared is $1/4$. So then we have:

julia> integral_squared = 1/4;

julia> ans = sqrt(integral_squared/xspan(modulated))
0.5

julia> ans ≈ myrms(modulated)
true

So it is correct.