裏目小僧の部屋

z変換で周波数特性を得るjavascript作成奮戦記

最終的に使っているのはこのページではなく→z変換とラプラス変換で周波数特性を得るjavascript

お名前
件名
本文

問題

  • javascriptには複素数がない。よってZの式をそのままevalで処理出来ない
  • 文字列の数式を処理する複素数クラスというものもmath.jsにはあるようだが外部ライブラリを使うのは嫌だ。いつまで動くか判らないのだから。
  • だから自前で計算させよう

 作成予定 Z変換式を入力して周波数特性を得る

方針:

pascalでデバッグしながら作りJavaScriptにして動かす
その為にもJavaScriptにしかない機能やライブラリ等に頼らず自前で処理する
複雑にならず、視認性の良い範囲で表現出来る事

対応する式

 Z z      大文字Zは1/z
 + - ^  ^はべき乗
 * /
 () sin() cos() tan() exp() fs 数字 PI π
   πを入れようと思ったけど、Unicodeにはたくさんありすぎるしサロゲートペアだから一旦外す
 Zの後の数字はべき乗とみなす(整数のみ)
 Zの前の*は省略可能

対応する変数

 Z式の前に lab=式;の形で定義を設定可能とし Z式中でラベルを使える
 ラベルは英字1文字+数値とする
   一部のラベルは 見やすくするために用途を決める
   ZnはZ^nと解釈されラベルとして使えない zも同じ
   fnは周波数 fはスライドバーで設定される事を想定する
   gnはゲイン gはスライドバーで設定される事を想定する
   Qnは Q値   Qはスライドバーで設定される事を想定する
    (分子)/(分母)の単純な形の時は
   anは 分子の係数
   bnは 分母の係数
   ラベルの定義は
   an=式;
   スライドバーで設定値を与える場合は
   an:最小式:最大式=式
    =の右側がスライドバーを表示する時の初期値 最小式最大式で最大最小値を設定する
     対数的に動かさないと違和感があるので、実際のスライドバーの値はlogで設定させる
   GUIで設定値を与える場合は
   an:=式
    =の右側が初期値  何か設定が必要なら an:設定=式 とする予定

pass1

  式の結果は全て実数に置換し ()と+ - * / Zのみ残す
   zはZ^-1と置換される
   Zの指数部が負のフィルタは作れないが、FIRフィルタのフラット性を見る用途にエラーにしない
  結果をZの添え字, 数字  () + - * / ^で分割した配列とする
  

pass2

  fを0〜fs/2の範囲で
  Z=cos(2*PIf/fs)-i*sin(2*PIf/fs)
  として結果を求めるJacaScriptのコードを吐き出す

JavaScript側のおよその方針

  • 20年前とは別物のように機能が上がっているから勉強しながらになる
  • この処理に使えそうな関数は string.replace のコールバック呼び出し
    • RegExp.exec()と同じ事だけど、こちらはPascalで似たことをするのがより面倒そう
  • 正規表現で全要素をコールバックさせて式を分解し、それを再帰下降で処理する

 最初はpascal上で動かす

  • デバッグは慣れたpascal上が楽だからね
  • つまりJavaScriptで動かすシミュレータ的な使い方をする
  • 最初は 正規表現unitのRegexprからreplaceに似た処理をしてくれるものを作る必要がある
  • replaceは3つの機能がある
    • 単なる文字列の置換 repelace("文字列","文字列")
    • 正規表現から置換文字列への置換 repelace(/正規表現/g,"文字列")
    • 正規表現でコールバック関数での置換 repelace(/正規表現/g,function)
  • 今回はreplaceは正規表現でしか使わないから最初のものは外す
  • replaceをセパレーターとして使う場合,コールバック内でsubstringを使う事になるからこれも必要

substring関数

//JavaScriptの  substringに似せる
function substring(s: string; st, ed: integer): string;
begin
  Result := copy(s, st, ed - st);
end;

replace関数

// -- jsのreplaceと似た動作をさせるために;
//https://regex.sorokin.engineer/en/latest/tregexpr.html
// ただし jsはs.replace(正規表現,rep)となるのでブラケットを外す等の手作業は必要

function replace(s: string; regx, rep: string): string; overload;
var n: integer;
  regopt: string;
  opt: TRegexReplaceOptions;
begin
  opt := [rroUseSubstitution];
  if copy(regx, 1, 1) = '/' then
  begin
    Delete(regx, 1, 1);
    for n := length(regx) downto 1 do if regx[n] = '/' then break;
    if regx[n] <> '/' then raise Exception.Create('/reg/<- ? ');

    regopt := copy(regx, n + 1, 10);
    regx   := copy(regx, 1, n - 1);
    for n := 1 to length(regopt) do case regopt[n] of
        'g': Include(opt, rroModifierG);
        'i': Include(opt, rroModifierI);
        'r': Include(opt, rroModifierR);
        's': Include(opt, rroModifierS);
        'm': Include(opt, rroModifierM);
        'x': Include(opt, rroModifierX);
      end;
  end;
  Result := Regexpr.ReplaceRegExpr(regx, s, rep, opt);
end;

type TRepFunction = function(match: string; pn: array of string; offset: integer; alls: string): string of object;
  TARegExprString = array of RegExprString;

  { TMyRegExpr }

  TMyRegExpr = class(TRegExpr)
    function MyCallRepFunc(ARegExpr: TRegExpr): RegExprString;
  public
    MyRepFunc: TRepFunction;
    AllStr: string;
    function a_pn(): TARegExprString;
    procedure setOpt(regx: string);

  end;
function replace(s: string; regx: string; repFunc: TRepFunction): string; overload;
var n: integer;
  regopt: string;
begin
  with TMyRegExpr.Create do try
      MyRepFunc := repFunc;
      AllStr    := s;
      setOpt(regx);
      Result := ReplaceEx(s, MyCallRepFunc);
    finally
      Free;
    end;
end;




{ TMyRegExpr }
procedure TMyRegExpr.setOpt(regx: string);
var n: integer;
  regopt: string;
begin
  if copy(regx, 1, 1) = '/' then
  begin
    Delete(regx, 1, 1);
    for n := length(regx) downto 1 do if regx[n] = '/' then break;
    if regx[n] <> '/' then raise Exception.Create('/reg/<- ? ');

    regopt := copy(regx, n + 1, 10);
    regx   := copy(regx, 1, n - 1);
    for n := 1 to length(regopt) do case regopt[n] of
        'i': ModifierI := True;
        'r': ModifierR := True;
        's': ModifierS := True;
        'g': ModifierG := True;
        'm': ModifierM := True;
        'x': ModifierX := True;
      end;
  end;
  Expression := regx;
end;
function TMyRegExpr.a_pn(): TARegExprString;
var
  i: integer;
begin
  Setlength(Result, 0);
  if SubExprMatchCount > 0 then
  begin
    Setlength(Result, SubExprMatchCount);
    for i := 0 to High(Result) do
    begin
      Result[i] := Match[i + 1];
    end;
  end;

end;
function TMyRegExpr.MyCallRepFunc(ARegExpr: TRegExpr): RegExprString;
begin
  Result := MyRepFunc(Match[0], slice(a_pn, SubExprMatchCount), MatchPos[0], AllStr);
end;


  • この最初のreplaceで空白を全部除いて
  • 次のreplaceで最初に "="と";"でセパレートし
  • 次に数式をreplaceで分解して処理する
  • ただしコールバック関数の引数に注意しなければならない
    • JavaScriptではコールバック関数の引数は
function (match, p0,p1,offset,astr) という順で可変長にしなければいけない
厄介なのは
・p0,p1,p2,p3...は配列ではなく 正規表現内の括弧数で決まる事
 よってJavaScriptに変換した後に置換が必ず必要になる。
 またpascalではコールバックに関数内関数が指定出来ない事も地味に制限となる
対策としては
var AllStr:string; 
function hoge(match, p0,p1:string;offset:Integer):string;

と関数内関数として

  {.J}with TMyRegExpr.Create do try
       setOpt(zRegSep);
       AllStr := s;
       if Exec(s) then   repeat
           hoge(Match[0],  Match[1], Match[2],MatchPos[0]);
         until not ExecNext;
     finally
       Free;
     end;{s.replace( zRegSep, hoge);} 
のように呼び出す事だ


 Pascal側はある程度動く - 裏目小僧 (2023年03月10日 14時22分21秒)

  (-0.1+Z)/(1-0.1*Z)
という入力に対して
let w = 2 * Math.PI  * f / fs;
let Zc=[1, Math.cos(w)];
let Zs=[0,-Math.sin(w)];
let c1 =1-0.1 *Zc[1];
let s1 = -0.1 *Zs[1];
let c0 =-0.1+Zc[1];
let s0 =Zs[1];
{let ww = c1*c1 + s1*s1;
let cc = c0;
c0 =( cc*c1 + s0*s1)/ww;
s0 =(-cc*s1 + s0*c1)/ww;
};
RAD  = Math.atan2(s0, c0);
POW  = Math.hypot(s0, c0);
とJavaScriptのコードを吐き出すまでは作れた。

しかし、結構コード量が多い。これをJavaScriptに変換する事を考えると ちょっと頑張る必要があるな

 とりえずPascal側はこれでおいて - 裏目小僧 (2023年03月11日 05時06分34秒)

  • 出来てる事
  1. 数式を計算させて係数を求める
  2. Zの式から周波数応答(ゲインと位相)を得る
  3. またJavaScript側でevalかFunctionで直接結果が得られる文字列の作成
  4. そのグラフ表示(JavaScriptでは描画関数等が違うので簡易に)
  • 1000行付近あるpascalソースだけどJavaScriptへの変換を始めよう
  • 現時点のLazarusソースを添付z_js_test20230311PasSrc.zip(8)

 グラフ表示部のJavaScript化 - 裏目小僧 (2023年03月17日 06時56分42秒)

  • ClipBd電卓にz_js_test.pasをShiftJisに変換して引数にすればjsを出力するようにした
  • Pas2JavaSc.pasにrepeat until/case等もJavaScriptに変換するように修正した
  • グラフ表示は結局手動で修正する事にした
  • グラフ表示をさせて計算部に色々バグがある事が判りデバッグ開始
  • 現時点のLazarusソースz_js_test20230317PasSrc.zip(9)
  • これから数式処理部をJsに置換する作業にかかる

 関数内関数にする - 裏目小僧 (2023年03月17日 15時15分43秒)

  • オブジェクト変数にするとthisが大量につくので鬱陶しい
  • 問題はreplace のコールバックを関数内関数に出来ない事
 {.J}with TMyRegExpr.Create do try
       setOpt(zRegSep);
       AllStr := s;
       if Exec(s) then   repeat
           ZPass1DataR(Match[0], slice(a_pn, SubExprMatchCount), MatchPos[0], AllStr);
         until not ExecNext;
     finally
       Free;
     end;{s.replace( zRegSep, ZPass1DataR);}    

と、コールバックを展開するようにした。

  • 上のTMyRegExprも置き換えている

 やっと動いた - 裏目小僧 (2023年03月19日 16時11分06秒)

Zfunc.js は z_js_test.pas を ClipBd電卓.exe にDropして作成したもの
PaintLogLev.jsは同じく作成した後にグラフ表示部だけ手直ししたもの
Ztes1_js.html はテスト用のhtmlファイル
  • まだテストは十分じゃないけど、全体が動いて嬉しい
  • で、作った後で思ったのだけど、これJavaScript側は正規表現でチェックだけして複素数の手前の数式処理もeval(Function)でやれば、もう少し短いコードになって楽だったかもしれない。

 −の処理でミス - 裏目小僧 (2023年03月24日 04時58分34秒)

1-2+3の時 1+(-2)+3としなければならない事を忘れていた左側が-で数字やラベルを得たら、その時点で-を付けて右を処理し左数字-右数字と戻ってきたら左数字+右数字とするように変更しなければ

 ラプラス変換の伝達関数を求める方に統一させる事にした - 裏目小僧 (2023年03月28日 10時24分42秒)

  • JavaScriptでラプラス変換の伝達関数を求めるをさらに改良して直接Z変換を求めるようにした。
  • そうするとラプラス変換との差異は小さいので両方を得る関数に統一できる頃に気づいた。
  • なら統一するしかない。
  • そもそもzは 1遅延演算子で ラプラス変換だと e^s で表現できるものだものね

プライバシーポリシー本文は日本語以外に翻訳禁止