#はじめに結論を書きます。
###しようとしたこと
オペアンプ回路の素子パラメータの最適化を機械学習で行おうとした。
###できたこと
Jupyter Notebook上からLTSpice(回路シミュレータ)を動かし、各種シミュレーションを実行し、結果を取得し、性能を評価し、新しいパラメータを決定し、ネットリストを書きかえ、シミュレーションを繰り返すプログラムを実装できた。
###できなかったこと
目標値が全然収束しません!(パラメータ数が多すぎるから?)
###Who I am
プログラミング、アナログ回路、機械学習すべて初心者です。
GpyOptのkernelの部分とか特に分かっていないです。
とにかく動かすことを目標としました。
何かおかしい部分があれば、コメントいただけると嬉しいです。
###ネットリストや書いたコード
GitHubに上げました。
汚くて不必要な部分もあると思います...
もしかしたら間違っている部分もあるかもしれません...
測定用回路のネットリストも上げてます。
ちなみにこの回路を組むのも結構キツかった...
###動作環境
OS: Ubuntu 18.04.2 LTS
ubuntuでLTSpiceを動かすためにwineを使っています。
Jupyter Notebook: 5.7.8
Python: 3.7.3
#アナログ回路設計と機械学習
###アナログ回路とは?
- アナログ回路は、電子回路の一種です。
- 電子回路は、「トランジスタ」を使っている回路のことを言います。
- PCやスマートフォンを始めとして、家電/ガジェットなど身の回りの多くの製品に電子回路は絶対入ってます。
- 電子回路は、デジタル回路とアナログ回路に分けられます。何が、違うか?
デジタル回路: 「0(L)」と「1(H)」の2つの値(離散値)しか扱わない
アナログ回路: 連続的な値を扱う
例えてみると...
0-99歳までの人々がいたときに
デジタルの世界では、0-49歳までと50-99歳の2つに分けて、
低いと高いかだけを扱う世界です。
一方で、アナログの世界では、0歳と1歳どころか、
0歳と0歳0.0000000....1秒も扱います。
###アナログ回路が行うこと
- 一定の電圧や電流を出力する
- アナログデータをデジタルデータに変換する
- 特定の周波数帯域のデータを通す
- 特定の周波数で電圧の高い状態と低い状態を繰り返す信号を出す
......様々なことを行います。
###回路設計の自動化
- デジタルは自動化が進む一方で、アナログは人の手による設計が残っています。
なぜか?
→ 回路動作の完璧な予測が難しく、より多くの手間・思考が必要なためです。
アナログは連続値を扱うので、入力が変われば、出力も常に変化します。
加えて、回路に使われる素子のばらつきにも影響を受けます。
###アナログ回路設計への機械学習の利用
そうはいっても、使う時間・コストは減らしたいはず。
そこで、機械学習を用いたアナログ回路設計が研究されているみたいです。
ただし、ググってみた限り、そんなに出てこないです。
多分各社、内部では相当やっているんだろうけど、情報を出したくないのか...。
群馬大の学生さんの論文はめちゃくちゃ参考になりました!
#設計対象と行ったこと
###設計対象は「演算増幅器(オペアンプ)」です。
対象としたのは、アナログ回路では、特に重要な回路である「オペアンプ」です。
オペアンプは2つの入力電圧の差を増幅し出力する回路です。
オペアンプがなぜ重要なのかというと...
・周辺の回路を組むことにより、様々な機能を実現できるから。
・「負帰還」効果により、より高精度で安定した動作を実現できるから。
このようなメリットがあるため、多くの回路で使われています。
###行ったこと
今回行った、いや、行おうとしたことは、素子パラメータの最適化です。
回路設計は、
1, 素子の構成を決める部分(素子をどのように繋げて目的の機能を実現するか?)
2, 素子のサイズ(長さ、幅...)を決める部分
に分かれますが、このプログラムでは「2」を行おうとしました。
使用したオペアンプの回路はこんな感じです。
シンプルな構成の回路だと思います。
#回路シミュレーション・性能評価
###LTSpiceによる回路シミュレーション
LTSpiceは、Analog Devicesという半導体メーカーが無償で提供しているSPICE電子回路シミュレーションソフトです。
・回路シミュレータにより、コンピュータ上で、回路特性の計算ができます。
(各部分に流れる電流、電圧やその時間応答、周波数依存、温度特性など...)
・SPICEは、電子回路シミュレータの種類の一つです。コードが公開されているため、このコードを元に、各社からソフトが開発されています。LTSpiceも、その中の一つです。
回路情報(素子をどのように繋ぐか、素子パラメータ、どのような測定をするか)は、「ネットリスト」に記述します。
このプログラムでは、ネットリストの中の素子パラメータの値を変えてシミュレーションを繰り返しました。
###オペアンプ回路の性能評価
オペアンプの評価は、「演算増幅器設計コンテスト」で使われているスペック・評価値を用いました。
コンテスト部門1の評価値が最も大きくなるときの、素子パラメータの組み合わせを求めることを目標にしました。
- スペック
評価項目 | 要件 |
---|---|
消費電流 | 規定条件下にてバイアス電流の変動が50%以下 |
消費電力 | 100mW以下 |
直流利得 | 40dB以上 |
位相余裕 | 45∘以上 |
利得帯域幅積 | 1MHz以上 |
スルーレート | 立ち上がり立ち下がりともに絶対値が0.1V/μs以上 |
全高調波歪 | 1%以下 |
同相除去比 | 40dB以上 |
電源電圧変動除去比 | VDD側, VSS側いずれか悪い側の0.1Hzでの値が40dB以上 |
出力電圧範囲 | 0Vを中心とする出力電圧が正負電源電圧の5%以上 |
同相入力範囲 | 0Vを中心とする出力電圧が正負電源電圧の5%以上 |
占有面積 | 1mm2以下 |
全高調波歪は、評価していません。実装はしてます。 | |
→ シミュレーションに時間がかかるため。 |
- 評価式
部門1: (スルーレート×同相入力範囲×直流利得)/消費電流
#プログラムの目的・手段
###目的
コンテスト部門1の評価値が最も大きくなるときの、素子パラメータの組み合わせ(各トランジスタのゲート幅・ゲート長、コンデンサの容量、抵抗の抵抗値)を求める。
###手段
GpyOptを用いたベイズ最適化により、評価関数(コンテスト部門1の評価式)が最大になるときの素子パラメータの組み合わせを求める。
#プログラムの処理手順
以下の図のようになります。
myBayseOpt = BayesianOptimization(f=B.opamp_f, domain=domain,
acquisition_type="LCB", kernel=kernel,
#constraints=constraints,
exact_feval=True)
for i in range(N):
myBayseOpt.run_optimization(max_iter=1)
print(breakCondition(myBayseOpt.Y, eps=eps))
if all(breakCondition(myBayseOpt.Y, eps=eps)):
break
この部分で、実際にベイズ最適化を行っています。
「f=B.opamp_f」と記述してあるように、
opamp_fからreturnされる出力値に対して、最小値を探索します。
1回ずつ確認を行いepsで指定された精度で収束したときに、breakします(するはずです)。
opamp_f内に書かれている処理はこちらです。
class BayseOpt_set:
def opamp_f(self, x):
x_dict = {}
##パラメータ挿入
print('------------------------')
for param, x__ in zip(self.params, x[0]):
x_dict[param[0]] = x__
print(param[0], x__)
print('------------------------')
keys = list(x_dict.keys())
for key in keys:
if "_" in key:
s = key.split("_")
for s_ in s:
x_dict[s_] = x_dict[key]
##ネットリストの書きかえ
my_analysis_opamp = Analysis_Opamp(file_dir=self.file_dir, count=self.optCount)
my_analysis_opamp.prepare_run(x_dict = x_dict)
self.analysis_results = my_analysis_opamp.run_analysis()
print(self.analysis_results)
self.check_result = self.check_spec()
self.section_results = self.eval_section_result()
print(self.section_results)
str_target_section = "eval" + str(self.target_section)
target_section_result = self.section_results[str_target_section]
self.X.append(x[0])
self.Y.append(target_section_result)
self.plotGraph()
self.logTxt()
return [target_section_result]
###テンプレートのコピー・ネットリストの書き換え
def opamp_f(self, x):
my_analysis_opamp.prepare_run(x_dict = x_dict)
class Analysis_Opamp:
def prepare_run(self, x_dict): ##templateのコピーとパラメータ書き換え
##templateのコピー
try:
os.mkdir(self.wk_dir)
except:
pass
file_names = os.listdir(self.file_dir + "/template_net")
for file_name in file_names:
from_file_path = self.file_dir + "/template_net/" + file_name
to_file_path = self.wk_dir + "/" + file_name
if (os.path.isfile(from_file_path)):
shutil.copy(from_file_path, self.wk_dir)
shutil.copy(self.file_dir + "/base/opamp-ML.txt",self.file_dir + "/" + str(self.count).zfill(6) + "/opamp-ML.txt")
##パラメータ書き換え
with open(self.wk_dir + "/opamp-ML.txt", "r") as f:
s = f.read()
lines = s.split("\n")
...(省略)...
wr_s = "\n".join(lines)
with open(self.wk_dir + "/" + file_name, "w") as f:
f.write(wr_s)
ここでは、以下のことが行われています。
1, 毎回のシミュレーション結果が格納されるフォルダ(000001,000002,000003...)が作られます。
2, template_netの中身(各特性の測定回路のネットリストが入っている)が全てコピーされ、opamp部分の記述が入っているtxt(opamp-ML.txt)もコピーされる。
3, opamp-ML.txtのパラメータ部分を書き換えて更新
4, opamp-ML.txtの内容を各ネットリストに書き込んで更新
###各種シミュレーションの実行
self.analysis_results = my_analysis_opamp.run_analysis()
def run_analysis(self):
tmp = self.analysis_PowerCom()
self.analysis_results["DeltaComCurrent"] = tmp[0]
self.analysis_results["PowerCom"] = tmp[1]
self.analysis_results["RefCurrent"] = tmp[2]
self.analysis_results["RefPowerCom"] = tmp[3]
tmp = self.analysis_DCGain()
self.analysis_results["DCGain_sim"] = tmp[0]
self.analysis_results["PhaseMargin"] = tmp[1]
...
return self.analysis_results
下でも、書いていますが、ここが一番苦労しました。
ここで、LTSpiceのシミュレーションを実行させ、結果を取得しています。
例として、CMRRのシミュレーション部分を載せておきます。
def analysis_CMRR(self): ##CMRR
#フォルダの作成・ネットリストのコピー
try:
os.mkdir(self.wk_dir + "/CMRR")
shutil.copy(self.wk_dir + "/tsmc018.lib", self.wk_dir + "/CMRR/tsmc018.lib")
except:
pass
os.chdir(self.wk_dir + "/CMRR")
file = self.wk_dir + "/CMRR/test-OpAmp-CMRR"
shutil.copyfile(self.wk_dir + "/test-OpAmp-CMRR.net", file + ".net")
#シミュレーションの実行
output = subprocess.run(["wine", "/hogehoge/LTSpice/XVIIx64.exe", "-b","test-OpAmp-CMRR.net"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#logファイルをテキスト処理して結果の取得
with codecs.open(file + ".log", "r", "Shift-JIS", "ignore") as file:
s = file.read()
lines = s.split("\n")
for line in lines:
if "cmrr:" in line:
cmrr = line
words = re.split('[|(|)|,|]| |\r',cmrr)
words = [x for x in words if x]
for word in words:
if "dB" in word:
CMRR_value = float(word.strip("dB"))
os.chdir(self.wk_dir)
return CMRR_value
各特性の結果を格納するフォルダ(ここでは"CMRR")を作り、中にネットリスト(ここでは"test-OpAmp-CMRR.net")をコピーして、subprocessでLTSpiceを回して、".log"ファイルの中身をテキスト処理して結果を取得しています。
詳しい実装方法については、別記事に記載しています。
結果は辞書型で以下のように返ってきます。
{'DeltaComCurrent': 25.955784905881647, 'PowerCom': 0.0017720372999999998, 'RefCurrent': 0.000426325, 'RefPowerCom': 0.0012789750000000001, 'DCGain_sim': 56.5882, 'PhaseMargin': 77.695, 'GB': 31505200.0, 'outR_sim': 11.182246208190918, 'outR': 4670.211033191379, 'DCGain': 58.41105737741378, 'CMRR': 73.2025, 'PSRR': 65.1247, 'VICM': 97.4045, 'VAMP': 1.422135, 'Out_V_Range': 100.0, 'SR': 13.4006, 'Noise': 0.0227766, 'Area': 1.21487219260711e-09}
###性能の評価
self.check_result = self.check_spec()
self.section_results = self.eval_section_result()
class BayseOpt_set:
def check_spec(self):
spec = {'DeltaComCurrent': 50,
'PowerCom': 0.1,
'PhaseMargin': 45,
'GB': 1e6,
'DCGain': 40,
'CMRR': 40,
'PSRR': 40,
'VICM': 5,
'Out_V_Range': 5,
'SR': 0.1,
'Area': 1e-6}
eval_spec = 0
if self.analysis_results['DeltaComCurrent'] <= spec['DeltaComCurrent']:
eval_spec += 1
if self.analysis_results['PowerCom'] <= spec['PowerCom']:
eval_spec += 1
...(省略)...
if self.analysis_results['Area'] <= spec['Area']:
eval_spec += 1
return eval_spec
def eval_section_result(self):
##部門1
self.section_results['section1_value'] = (self.analysis_results['SR']*1e6) * self.analysis_results['VICM'] * math.exp(self.analysis_results['DCGain']/20) / self.analysis_results['RefCurrent']
self.section_results['section2_value'] = (self.analysis_results['GB'] * self.analysis_results['PhaseMargin']) / ((self.analysis_results['RefPowerCom']**2) * self.analysis_results['outR'] * self.analysis_results['Noise'])
...(省略)...
##Gpyは最小値を探索する
self.section_results['eval1'] = -1 * (self.check_result - 10.5) * abs(self.section_results['section1_value']) ##absを付けておいて、マイナス×マイナスになるのを防ぐ
self.section_results['eval2'] = -1 * (self.check_result - 10.5) * abs(self.section_results['section2_value'])
...(省略)...
return self.section_results
check_spec()で、返ってきた結果が「特性要件」(満たすべき必須要件)を満たしているかをチェックしています。
各項目、達成していれば、eval_specが+1され、全項目(全高調波歪除く)を満たしていると、11になります。
次にeval_section_result()でベイズ最適化の対象となる関数のXに対する出力値Yを算出しています。
self.section_results['section1_value'] は、コンテスト部門1の評価値ですが、ベイズ最適化の対象は、この値に対して、特性要件を満たしているかどうかを加味し("(self.check_result - 10.5)"の部分)、さらにGPyoptは最小値を探索するので、-1を前にかけた値(self.section_results['eval1'])をreturnして、この値をYとしています。
この辺が少しマズい気はする...
###次のパラメータ値の決定
returnされたtarget_section_resultの値に対して、Gpyoptによるベイズ最適化が行われます。
次のパラメータが決定すると、またopamp_fが実行されるので、シミュレーションが繰り返されます。
#苦労したポイント
Jupyter NotebookとLTSpiceの連携です。
シミュレーションの実行は「subprocess」で行いました。
データの取り出しは、".log"ファイルであれば、テキストで読めるのですが、
だけは読めず、ライブラリを使用して解決しました。
→ 別記事書きました。https://qiita.com/dl10yr/items/a4f1710f5f8d9fdcb810
#50回くらい試行した結果
収束しない!
#まとめ
- とにかく動かすことができた。良かった。
- 次元数が多すぎる → 手計算における手法を使って次元数を下げるか、他の方法があるか...
- 手計算を使うと、機械学習の"意味"が減ってしまう...
- 細かいバグ取りが終わっていない→ logファイルをテキスト処理して結果を取得するときに、上手く取り出せず、そこでプログラムが停止する可能性がある。
#参考文献
2018年演算増幅器コンテスト
電子回路設計の基礎 ~1章: アナログ回路について~
猫でも分かるCMOSアナログ回路
Pythonでベイズ最適化を行うパッケージ GPyOpt
ベイズ最適化シリーズ(1) ーベイズ最適化の可視化ー
高性能演算増幅器の設計 - 群馬大学
平成27年度 修士論文 機能ブロックの組み合わせによる演算増幅器の自動設計